diff --git a/apps/api-runner/node_modules/.bin/tsc b/apps/api-runner/node_modules/.bin/tsc
index bc3dbe24..6d8d1011 100755
--- a/apps/api-runner/node_modules/.bin/tsc
+++ b/apps/api-runner/node_modules/.bin/tsc
@@ -1,17 +1,43 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
+basedir_win="$basedir"
+exe=""
+msys=""
-case `uname` in
- *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
+case `uname -a` in
+ *CYGWIN*|*MINGW*|*MSYS*)
+ if command -v cygpath > /dev/null 2>&1; then
+ basedir_win=`cygpath -w "$basedir"`
+ fi
+ exe=".exe"
+ msys="true"
+ ;;
+ *WSL2*)
+ if command -v wslpath > /dev/null 2>&1; then
+ basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
+ if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
+ basedir_win="$basedir"
+ else
+ exe=".exe"
+ fi
+ fi
+ ;;
esac
if [ -z "$NODE_PATH" ]; then
- export NODE_PATH="/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/node_modules"
+ export NODE_PATH="/Users/max/Works/Personal/mydevtools/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/max/Works/Personal/mydevtools/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/max/Works/Personal/mydevtools/node_modules/.pnpm/node_modules"
else
- export NODE_PATH="/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/node_modules:$NODE_PATH"
+ export NODE_PATH="/Users/max/Works/Personal/mydevtools/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/max/Works/Personal/mydevtools/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/max/Works/Personal/mydevtools/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
-if [ -x "$basedir/node" ]; then
+if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
+ exec "$basedir/node.exe" "$basedir_win/../typescript/bin/tsc" "$@"
+elif [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
+elif command -v node >/dev/null 2>&1; then
+ exec node "$basedir/../typescript/bin/tsc" "$@"
+elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
+ exec node.exe "$basedir_win/../typescript/bin/tsc" "$@"
else
exec node "$basedir/../typescript/bin/tsc" "$@"
fi
+# cmd-shim-target=/Users/max/Works/Personal/mydevtools/apps/api-runner/node_modules/typescript/bin/tsc
diff --git a/apps/api-runner/node_modules/.bin/tsserver b/apps/api-runner/node_modules/.bin/tsserver
index 03c2f186..66a860ca 100755
--- a/apps/api-runner/node_modules/.bin/tsserver
+++ b/apps/api-runner/node_modules/.bin/tsserver
@@ -1,17 +1,43 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
+basedir_win="$basedir"
+exe=""
+msys=""
-case `uname` in
- *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
+case `uname -a` in
+ *CYGWIN*|*MINGW*|*MSYS*)
+ if command -v cygpath > /dev/null 2>&1; then
+ basedir_win=`cygpath -w "$basedir"`
+ fi
+ exe=".exe"
+ msys="true"
+ ;;
+ *WSL2*)
+ if command -v wslpath > /dev/null 2>&1; then
+ basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
+ if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
+ basedir_win="$basedir"
+ else
+ exe=".exe"
+ fi
+ fi
+ ;;
esac
if [ -z "$NODE_PATH" ]; then
- export NODE_PATH="/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/node_modules"
+ export NODE_PATH="/Users/max/Works/Personal/mydevtools/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/max/Works/Personal/mydevtools/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/max/Works/Personal/mydevtools/node_modules/.pnpm/node_modules"
else
- export NODE_PATH="/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/node_modules:$NODE_PATH"
+ export NODE_PATH="/Users/max/Works/Personal/mydevtools/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/max/Works/Personal/mydevtools/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/max/Works/Personal/mydevtools/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
-if [ -x "$basedir/node" ]; then
+if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
+ exec "$basedir/node.exe" "$basedir_win/../typescript/bin/tsserver" "$@"
+elif [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
+elif command -v node >/dev/null 2>&1; then
+ exec node "$basedir/../typescript/bin/tsserver" "$@"
+elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
+ exec node.exe "$basedir_win/../typescript/bin/tsserver" "$@"
else
exec node "$basedir/../typescript/bin/tsserver" "$@"
fi
+# cmd-shim-target=/Users/max/Works/Personal/mydevtools/apps/api-runner/node_modules/typescript/bin/tsserver
diff --git a/apps/backend/app/api/router.py b/apps/backend/app/api/router.py
index 1b9361c8..47456993 100644
--- a/apps/backend/app/api/router.py
+++ b/apps/backend/app/api/router.py
@@ -23,6 +23,8 @@
from app.api.routes.redis_commander.api import router as redis_commander_router
from app.api.routes.url_shortener.api import router as url_shortener_router
from app.api.routes.dns_lookup.api import router as dns_lookup_router
+from app.api.routes.whois.api import router as whois_router
+from app.api.routes.webhook_tester.api import router as webhook_tester_router
from app.api.routes.audit_log.api import router as audit_log_router
from app.api.routes.workspaces.api import router as workspaces_router
@@ -50,5 +52,7 @@
api_router.include_router(redis_commander_router)
api_router.include_router(url_shortener_router)
api_router.include_router(dns_lookup_router)
+api_router.include_router(whois_router)
+api_router.include_router(webhook_tester_router)
api_router.include_router(audit_log_router)
api_router.include_router(workspaces_router)
diff --git a/apps/backend/app/api/routes/api_client/services.py b/apps/backend/app/api/routes/api_client/services.py
index dac54d63..65001530 100644
--- a/apps/backend/app/api/routes/api_client/services.py
+++ b/apps/backend/app/api/routes/api_client/services.py
@@ -86,7 +86,6 @@ async def list_collections(*, ctx: WorkspaceContext) -> list[ApiClientCollection
async def create_collection(ctx: WorkspaceContext, body: ApiClientCollectionCreate) -> ApiClientCollectionOut:
doc: dict[str, Any] = {
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"name": body.name,
@@ -133,7 +132,6 @@ async def list_environments(*, ctx: WorkspaceContext) -> list[ApiClientEnvironme
async def create_environment(ctx: WorkspaceContext, body: ApiClientEnvironmentCreate) -> ApiClientEnvironmentOut:
doc: dict[str, Any] = {
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"name": body.name,
@@ -213,7 +211,6 @@ async def create_history(ctx: WorkspaceContext, body: ApiClientHistoryCreate) ->
ts = body.timestamp if body.timestamp is not None else int(time.time() * 1000)
doc: dict[str, Any] = {
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"method": body.method,
@@ -257,7 +254,7 @@ async def clear_history(ctx: WorkspaceContext) -> None:
# 2. Scoping by workspace_id would break the anonymous read path.
# 3. The mock_id token (~144 bits of entropy) is the access control boundary.
# Ownership is tracked by `created_by` only; the collection is NOT stamped with
-# org_id / workspace_id / owner_uid.
+# workspace_id / owner_uid.
def _mock_doc_to_out(doc: dict[str, Any]) -> ApiClientPublicMockOut:
@@ -315,7 +312,7 @@ async def get_public_mock_anonymous(mock_id: str) -> ApiClientPublicMockOut | No
#
# API_CLIENT_WORKSPACES stores the API Client tool's own "workspace" concept
# (grouping of API requests) — completely separate from our global Workspaces
-# feature. These are per-user data so they ARE stamped with org_id/workspace_id.
+# feature. These are per-user data so they ARE stamped with workspace_id.
def _ws_doc_to_out(doc: dict[str, Any]) -> ApiClientWorkspaceOut:
@@ -340,7 +337,6 @@ async def list_workspaces(*, ctx: WorkspaceContext) -> list[ApiClientWorkspaceOu
async def create_workspace(ctx: WorkspaceContext, body: ApiClientWorkspaceCreate) -> ApiClientWorkspaceOut:
doc: dict[str, Any] = {
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"name": body.name,
diff --git a/apps/backend/app/api/routes/api_key_vault/services.py b/apps/backend/app/api/routes/api_key_vault/services.py
index 19193bbe..dfb5db18 100644
--- a/apps/backend/app/api/routes/api_key_vault/services.py
+++ b/apps/backend/app/api/routes/api_key_vault/services.py
@@ -52,7 +52,6 @@ async def create_entry(ctx: WorkspaceContext, body: ApiKeyEntryCreate) -> ApiKey
doc: dict[str, Any] = {
"_id": eid,
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"encryptedData": body.encryptedData,
diff --git a/apps/backend/app/api/routes/bookmarks/services.py b/apps/backend/app/api/routes/bookmarks/services.py
index bcf3997c..9a066659 100644
--- a/apps/backend/app/api/routes/bookmarks/services.py
+++ b/apps/backend/app/api/routes/bookmarks/services.py
@@ -93,7 +93,6 @@ async def create_bookmark(ctx: WorkspaceContext, body: BookmarkCreate) -> Bookma
doc: dict[str, Any] = {
"_id": bid,
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"title": body.title,
@@ -155,7 +154,6 @@ async def import_bookmarks(ctx: WorkspaceContext, body: BookmarkImportBody) -> d
doc = {
"_id": fid,
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"name": folder.name,
@@ -174,7 +172,6 @@ async def import_bookmarks(ctx: WorkspaceContext, body: BookmarkImportBody) -> d
doc = {
"_id": bid,
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"title": bookmark.title,
@@ -237,7 +234,6 @@ async def create_folder(ctx: WorkspaceContext, body: BookmarkFolderCreate) -> Bo
doc: dict[str, Any] = {
"_id": fid,
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"name": body.name,
diff --git a/apps/backend/app/api/routes/code_snippets/services.py b/apps/backend/app/api/routes/code_snippets/services.py
index 13862f4c..8ce4a00d 100644
--- a/apps/backend/app/api/routes/code_snippets/services.py
+++ b/apps/backend/app/api/routes/code_snippets/services.py
@@ -53,7 +53,6 @@ async def create_code_snippet(ctx: WorkspaceContext, body: CodeSnippetCreate) ->
doc: dict[str, Any] = {
"_id": sid,
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"title": body.title,
diff --git a/apps/backend/app/api/routes/environment_manager/services.py b/apps/backend/app/api/routes/environment_manager/services.py
index 395ed5a5..84ffc5d9 100644
--- a/apps/backend/app/api/routes/environment_manager/services.py
+++ b/apps/backend/app/api/routes/environment_manager/services.py
@@ -52,7 +52,6 @@ async def create_entry(ctx: WorkspaceContext, body: EnvSetEntryCreate) -> EnvSet
doc: dict[str, Any] = {
"_id": eid,
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"encryptedData": body.encryptedData,
diff --git a/apps/backend/app/api/routes/json_formatter/services.py b/apps/backend/app/api/routes/json_formatter/services.py
index d2a60169..74698d33 100644
--- a/apps/backend/app/api/routes/json_formatter/services.py
+++ b/apps/backend/app/api/routes/json_formatter/services.py
@@ -81,7 +81,6 @@ async def create_document(ctx: WorkspaceContext, body: JsonFormatterDocumentCrea
now = datetime.now(timezone.utc)
doc: dict[str, Any] = {
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"title": body.title,
diff --git a/apps/backend/app/api/routes/nosql/services.py b/apps/backend/app/api/routes/nosql/services.py
index 8a147724..af6addd0 100644
--- a/apps/backend/app/api/routes/nosql/services.py
+++ b/apps/backend/app/api/routes/nosql/services.py
@@ -48,7 +48,6 @@ async def create_connection(ctx: WorkspaceContext, body: ConnectionCreate) -> Co
doc: dict[str, Any] = {
"_id": _id,
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"encryptedData": body.encryptedData,
diff --git a/apps/backend/app/api/routes/notes/services.py b/apps/backend/app/api/routes/notes/services.py
index 70a59644..fcd58d3c 100644
--- a/apps/backend/app/api/routes/notes/services.py
+++ b/apps/backend/app/api/routes/notes/services.py
@@ -82,7 +82,6 @@ async def create_note(ctx: WorkspaceContext, body: NoteCreate) -> NoteOut:
doc = {
"_id": note_id,
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"title": body.title or "Untitled",
diff --git a/apps/backend/app/api/routes/passwords/services.py b/apps/backend/app/api/routes/passwords/services.py
index dda7b130..a0482dbb 100644
--- a/apps/backend/app/api/routes/passwords/services.py
+++ b/apps/backend/app/api/routes/passwords/services.py
@@ -79,7 +79,6 @@ async def setup_vault(ctx: WorkspaceContext, body: VaultSetupRequest) -> VaultOu
doc: dict[str, Any] = {
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"salt": body.salt,
@@ -113,7 +112,6 @@ async def create_entry(ctx: WorkspaceContext, body: PasswordEntryCreate) -> Pass
doc: dict[str, Any] = {
"_id": eid,
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"encryptedData": body.encryptedData,
diff --git a/apps/backend/app/api/routes/redis_commander/services.py b/apps/backend/app/api/routes/redis_commander/services.py
index b32c1827..43347f57 100644
--- a/apps/backend/app/api/routes/redis_commander/services.py
+++ b/apps/backend/app/api/routes/redis_commander/services.py
@@ -52,7 +52,6 @@ async def create_connection(ctx: WorkspaceContext, body: RedisConnectionCreate)
doc: dict[str, Any] = {
"_id": _id,
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"encryptedData": body.encryptedData,
diff --git a/apps/backend/app/api/routes/s3_drive/services.py b/apps/backend/app/api/routes/s3_drive/services.py
index 20862756..200c1192 100644
--- a/apps/backend/app/api/routes/s3_drive/services.py
+++ b/apps/backend/app/api/routes/s3_drive/services.py
@@ -72,7 +72,6 @@ async def create_connection(ctx: WorkspaceContext, body: S3ConnectionCreate) ->
doc: dict[str, Any] = {
"_id": conn_id,
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"name": body.name,
diff --git a/apps/backend/app/api/routes/sql_client/services.py b/apps/backend/app/api/routes/sql_client/services.py
index 6d8209b4..1a67c081 100644
--- a/apps/backend/app/api/routes/sql_client/services.py
+++ b/apps/backend/app/api/routes/sql_client/services.py
@@ -53,7 +53,6 @@ async def create_connection(ctx: WorkspaceContext, body: SqlConnectionCreate) ->
doc: dict[str, Any] = {
"_id": _id,
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"encryptedData": body.encryptedData,
diff --git a/apps/backend/app/api/routes/tasks/services.py b/apps/backend/app/api/routes/tasks/services.py
index 8e0707ec..d53b6a33 100644
--- a/apps/backend/app/api/routes/tasks/services.py
+++ b/apps/backend/app/api/routes/tasks/services.py
@@ -185,7 +185,6 @@ async def create_task(ctx: WorkspaceContext, body: TaskCreate) -> TaskOut:
now = datetime.now(timezone.utc)
doc: dict[str, Any] = {
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"text": body.text,
@@ -270,7 +269,6 @@ async def import_tasks(ctx: WorkspaceContext, body: TaskImportRequest) -> dict[s
row.pop("createdAt", None)
row.pop("completedAt", None)
row["created_by"] = ctx.uid
- row["org_id"] = ctx.org_id
row["workspace_id"] = ctx.workspace_id
row["owner_uid"] = ctx.uid
row["createdAt"] = now
@@ -303,7 +301,6 @@ async def create_project(ctx: WorkspaceContext, body: ProjectCreate) -> ProjectO
now = datetime.now(timezone.utc)
doc = {
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"name": body.name,
diff --git a/apps/backend/app/api/routes/url_shortener/services.py b/apps/backend/app/api/routes/url_shortener/services.py
index 75d15e02..c4141d53 100644
--- a/apps/backend/app/api/routes/url_shortener/services.py
+++ b/apps/backend/app/api/routes/url_shortener/services.py
@@ -181,7 +181,6 @@ async def create_link(ctx: WorkspaceContext, body: ShortLinkCreate) -> ShortLink
"original_url": url,
"title": title,
"created_by": ctx.uid,
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
"owner_uid": ctx.uid,
"created_at": create_timestamp(),
@@ -254,7 +253,7 @@ async def record_click(code: str, ua: str = "", referrer: str = "") -> None:
# Fetch the link document to inherit workspace stamps
db = db_manager.get_db()
- link_doc = await db[COLLECTION].find_one({"_id": code}, {"org_id": 1, "workspace_id": 1, "owner_uid": 1})
+ link_doc = await db[COLLECTION].find_one({"_id": code}, {"workspace_id": 1, "owner_uid": 1})
event = {
"code": code,
@@ -267,8 +266,6 @@ async def record_click(code: str, ua: str = "", referrer: str = "") -> None:
# Inherit workspace stamps from the link
if link_doc:
- if "org_id" in link_doc:
- event["org_id"] = link_doc["org_id"]
if "workspace_id" in link_doc:
event["workspace_id"] = link_doc["workspace_id"]
if "owner_uid" in link_doc:
diff --git a/apps/backend/app/api/routes/webhook_tester/__init__.py b/apps/backend/app/api/routes/webhook_tester/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/backend/app/api/routes/webhook_tester/api.py b/apps/backend/app/api/routes/webhook_tester/api.py
new file mode 100644
index 00000000..06f2cdcd
--- /dev/null
+++ b/apps/backend/app/api/routes/webhook_tester/api.py
@@ -0,0 +1,76 @@
+# ponytail: public capability-URL tool — no auth on any endpoint (same as
+# dns_lookup). Bin ids are unguessable; the receive endpoint MUST stay open
+# because external services (Stripe, GitHub, ...) call it directly.
+from fastapi import APIRouter, HTTPException, Query, Request
+
+from app.api.routes.webhook_tester import services as svc
+from app.api.routes.webhook_tester.schema import BinOut, RequestListOut
+
+router = APIRouter(prefix="/webhook-tester", tags=["webhook-tester"])
+
+_ALL_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]
+
+
+@router.post("/bins", response_model=BinOut, summary="Create a webhook bin (expires after 24h)")
+async def create_bin() -> BinOut:
+ return BinOut(**await svc.create_bin())
+
+
+@router.api_route("/in/{bin_id}", methods=_ALL_METHODS, summary="Receive a webhook (public, any method)")
+@router.api_route(
+ "/in/{bin_id}/{path:path}",
+ methods=_ALL_METHODS,
+ summary="Receive a webhook with a path suffix (public, any method)",
+)
+async def receive(bin_id: str, request: Request, path: str = "") -> dict:
+ body = await request.body()
+
+ query: dict[str, str] = {}
+ for key, value in request.query_params.multi_items():
+ query[key] = f"{query[key]},{value}" if key in query else value
+
+ forwarded = request.headers.get("x-forwarded-for")
+ if forwarded:
+ source_ip: str | None = forwarded.split(",")[0].strip()
+ else:
+ source_ip = request.client.host if request.client else None
+
+ record = svc.make_record(
+ method=request.method,
+ path=path,
+ query=query,
+ headers=dict(request.headers),
+ body=body,
+ content_type=request.headers.get("content-type"),
+ source_ip=source_ip,
+ )
+ if not await svc.add_request(bin_id, record):
+ raise HTTPException(status_code=404, detail="Bin not found or expired")
+ return {"ok": True}
+
+
+@router.get(
+ "/bins/{bin_id}/requests",
+ response_model=RequestListOut,
+ summary="List captured requests (newest first)",
+)
+async def list_requests(
+ bin_id: str,
+ after: str | None = Query(default=None, description="Cursor: request id or ISO timestamp"),
+) -> RequestListOut:
+ result = await svc.list_requests(bin_id, after)
+ if result is None:
+ raise HTTPException(status_code=404, detail="Bin not found or expired")
+ meta, items = result
+ return RequestListOut(
+ bin_id=bin_id,
+ expires_at=meta["expires_at"],
+ count=len(items),
+ requests=items,
+ )
+
+
+@router.delete("/bins/{bin_id}/requests", status_code=204, summary="Clear captured requests")
+async def clear_requests(bin_id: str) -> None:
+ if not await svc.clear_requests(bin_id):
+ raise HTTPException(status_code=404, detail="Bin not found or expired")
diff --git a/apps/backend/app/api/routes/webhook_tester/schema.py b/apps/backend/app/api/routes/webhook_tester/schema.py
new file mode 100644
index 00000000..908c21c7
--- /dev/null
+++ b/apps/backend/app/api/routes/webhook_tester/schema.py
@@ -0,0 +1,31 @@
+from datetime import datetime
+
+from pydantic import BaseModel
+
+
+class BinOut(BaseModel):
+ bin_id: str
+ created_at: datetime
+ expires_at: datetime
+
+
+class RecordedRequest(BaseModel):
+ id: str
+ method: str
+ path: str = ""
+ query: dict[str, str] = {}
+ headers: dict[str, str] = {}
+ body: str = ""
+ body_encoding: str = "text" # "text" | "base64" (binary payloads)
+ body_truncated: bool = False
+ content_type: str | None = None
+ size: int = 0 # original body size in bytes (before truncation)
+ source_ip: str | None = None
+ received_at: datetime
+
+
+class RequestListOut(BaseModel):
+ bin_id: str
+ expires_at: datetime
+ count: int
+ requests: list[RecordedRequest]
diff --git a/apps/backend/app/api/routes/webhook_tester/services.py b/apps/backend/app/api/routes/webhook_tester/services.py
new file mode 100644
index 00000000..9fddb4f3
--- /dev/null
+++ b/apps/backend/app/api/routes/webhook_tester/services.py
@@ -0,0 +1,222 @@
+"""Webhook Tester — short-lived request bins.
+
+Storage: Redis with a 24h TTL when configured (matches the dominant
+short-lived-data pattern, see app/core/redis_client.py). When Redis is
+unavailable (dev/tests without REDIS_URL) an in-process fallback store with
+the same TTL semantics is used.
+"""
+from __future__ import annotations
+
+import base64
+import json
+import logging
+import secrets
+import uuid
+from datetime import datetime, timedelta, timezone
+from typing import Any
+
+from app.core.redis_client import get_redis
+
+log = logging.getLogger("app.webhook_tester")
+
+BIN_TTL_SECONDS = 24 * 60 * 60
+MAX_REQUESTS_PER_BIN = 100
+MAX_BODY_BYTES = 64 * 1024
+
+# bin_id -> {"bin_id", "created_at", "expires_at", "requests": [newest, ...]}
+_memory_bins: dict[str, dict[str, Any]] = {}
+
+
+def _now() -> datetime:
+ return datetime.now(timezone.utc)
+
+
+def _bin_key(bin_id: str) -> str:
+ return f"webhook_tester:bin:{bin_id}"
+
+
+def _req_key(bin_id: str) -> str:
+ return f"webhook_tester:req:{bin_id}"
+
+
+def new_bin_id() -> str:
+ """Random URL-safe id (~11 chars) — unguessable capability id."""
+ return secrets.token_urlsafe(8)
+
+
+def _purge_memory() -> None:
+ now_iso = _now().isoformat()
+ expired = [bid for bid, entry in _memory_bins.items() if entry["expires_at"] <= now_iso]
+ for bid in expired:
+ _memory_bins.pop(bid, None)
+
+
+def make_record(
+ *,
+ method: str,
+ path: str,
+ query: dict[str, str],
+ headers: dict[str, str],
+ body: bytes,
+ content_type: str | None,
+ source_ip: str | None,
+) -> dict[str, Any]:
+ """Build a stored request record; truncates large bodies, base64s binary ones."""
+ size = len(body)
+ truncated = False
+ if size > MAX_BODY_BYTES:
+ body = body[:MAX_BODY_BYTES]
+ truncated = True
+ try:
+ text = body.decode("utf-8")
+ encoding = "text"
+ except UnicodeDecodeError:
+ text = base64.b64encode(body).decode("ascii")
+ encoding = "base64"
+ return {
+ "id": uuid.uuid4().hex[:12],
+ "method": method.upper(),
+ "path": path,
+ "query": query,
+ "headers": headers,
+ "body": text,
+ "body_encoding": encoding,
+ "body_truncated": truncated,
+ "content_type": content_type,
+ "size": size,
+ "source_ip": source_ip,
+ "received_at": _now().isoformat(),
+ }
+
+
+async def create_bin() -> dict[str, Any]:
+ created = _now()
+ meta = {
+ "bin_id": new_bin_id(),
+ "created_at": created.isoformat(),
+ "expires_at": (created + timedelta(seconds=BIN_TTL_SECONDS)).isoformat(),
+ }
+ r = get_redis()
+ if r is not None:
+ try:
+ await r.set(_bin_key(meta["bin_id"]), json.dumps(meta), ex=BIN_TTL_SECONDS)
+ return meta
+ except Exception as exc: # noqa: BLE001 — fail over to memory store
+ log.warning("webhook_tester.redis.create failed err=%s", exc)
+ _purge_memory()
+ _memory_bins[meta["bin_id"]] = {**meta, "requests": []}
+ return meta
+
+
+async def get_bin(bin_id: str) -> dict[str, Any] | None:
+ """Return bin metadata or None if unknown/expired."""
+ r = get_redis()
+ if r is not None:
+ try:
+ raw = await r.get(_bin_key(bin_id))
+ if raw is not None:
+ return json.loads(raw)
+ except Exception as exc: # noqa: BLE001
+ log.warning("webhook_tester.redis.get_bin failed err=%s", exc)
+ _purge_memory()
+ entry = _memory_bins.get(bin_id)
+ if entry is None:
+ return None
+ return {k: entry[k] for k in ("bin_id", "created_at", "expires_at")}
+
+
+async def add_request(bin_id: str, record: dict[str, Any]) -> bool:
+ """Store a request (newest first, capped). False if the bin is unknown/expired."""
+ r = get_redis()
+ if r is not None:
+ try:
+ ttl = await r.ttl(_bin_key(bin_id))
+ if ttl is None or ttl <= 0:
+ return False
+ pipe = r.pipeline()
+ pipe.lpush(_req_key(bin_id), json.dumps(record))
+ pipe.ltrim(_req_key(bin_id), 0, MAX_REQUESTS_PER_BIN - 1)
+ pipe.expire(_req_key(bin_id), ttl)
+ await pipe.execute()
+ return True
+ except Exception as exc: # noqa: BLE001
+ log.warning("webhook_tester.redis.add failed err=%s", exc)
+ _purge_memory()
+ entry = _memory_bins.get(bin_id)
+ if entry is None:
+ return False
+ entry["requests"].insert(0, record)
+ del entry["requests"][MAX_REQUESTS_PER_BIN:]
+ return True
+
+
+def _parse_ts(value: str) -> datetime | None:
+ try:
+ dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
+ except (ValueError, AttributeError):
+ return None
+ if dt.tzinfo is None:
+ dt = dt.replace(tzinfo=timezone.utc)
+ return dt
+
+
+def filter_after(items: list[dict[str, Any]], after: str) -> list[dict[str, Any]]:
+ """Return only entries newer than the cursor (request id or ISO timestamp).
+
+ `items` is newest-first. Unknown cursors return everything (safe for polling).
+ """
+ newer: list[dict[str, Any]] = []
+ for item in items:
+ if item.get("id") == after:
+ return newer
+ newer.append(item)
+ ts = _parse_ts(after)
+ if ts is None:
+ return items
+ result = []
+ for item in items:
+ item_ts = _parse_ts(str(item.get("received_at", "")))
+ if item_ts is not None and item_ts > ts:
+ result.append(item)
+ return result
+
+
+async def list_requests(
+ bin_id: str, after: str | None = None
+) -> tuple[dict[str, Any], list[dict[str, Any]]] | None:
+ """Return (bin meta, requests newest-first) or None if the bin is unknown/expired."""
+ meta = await get_bin(bin_id)
+ if meta is None:
+ return None
+ items: list[dict[str, Any]] | None = None
+ r = get_redis()
+ if r is not None:
+ try:
+ raw = await r.lrange(_req_key(bin_id), 0, MAX_REQUESTS_PER_BIN - 1)
+ items = [json.loads(x) for x in raw]
+ except Exception as exc: # noqa: BLE001
+ log.warning("webhook_tester.redis.list failed err=%s", exc)
+ if items is None:
+ entry = _memory_bins.get(bin_id)
+ items = list(entry["requests"]) if entry else []
+ if after:
+ items = filter_after(items, after)
+ return meta, items
+
+
+async def clear_requests(bin_id: str) -> bool:
+ """Delete all captured requests. False if the bin is unknown/expired."""
+ meta = await get_bin(bin_id)
+ if meta is None:
+ return False
+ r = get_redis()
+ if r is not None:
+ try:
+ await r.delete(_req_key(bin_id))
+ return True
+ except Exception as exc: # noqa: BLE001
+ log.warning("webhook_tester.redis.clear failed err=%s", exc)
+ entry = _memory_bins.get(bin_id)
+ if entry is not None:
+ entry["requests"] = []
+ return True
diff --git a/apps/backend/app/api/routes/whois/__init__.py b/apps/backend/app/api/routes/whois/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/backend/app/api/routes/whois/api.py b/apps/backend/app/api/routes/whois/api.py
new file mode 100644
index 00000000..b7f2ffaf
--- /dev/null
+++ b/apps/backend/app/api/routes/whois/api.py
@@ -0,0 +1,14 @@
+# ponytail: stateless tool — no per-user persistence, no workspace scoping needed
+from fastapi import APIRouter, Query
+
+from app.api.routes.whois import services as svc
+from app.api.routes.whois.schema import WhoisResult
+
+router = APIRouter(prefix="/whois", tags=["whois"])
+
+
+@router.get("/lookup", response_model=WhoisResult, summary="RDAP/WHOIS lookup for a domain or IP address")
+async def lookup(
+ target: str = Query(..., min_length=1, max_length=300, description="Domain name or IP address to look up"),
+) -> WhoisResult:
+ return await svc.lookup_whois(target)
diff --git a/apps/backend/app/api/routes/whois/schema.py b/apps/backend/app/api/routes/whois/schema.py
new file mode 100644
index 00000000..7baac512
--- /dev/null
+++ b/apps/backend/app/api/routes/whois/schema.py
@@ -0,0 +1,23 @@
+from typing import Any, Literal
+
+from pydantic import BaseModel
+
+
+class WhoisContact(BaseModel):
+ roles: list[str] = []
+ name: str | None = None
+ organization: str | None = None
+ handle: str | None = None
+
+
+class WhoisResult(BaseModel):
+ target: str
+ type: Literal["domain", "ip"]
+ registrar: str | None = None
+ status: list[str] = []
+ created: str | None = None
+ updated: str | None = None
+ expires: str | None = None
+ nameservers: list[str] = []
+ contacts: list[WhoisContact] = []
+ raw: dict[str, Any] = {}
diff --git a/apps/backend/app/api/routes/whois/services.py b/apps/backend/app/api/routes/whois/services.py
new file mode 100644
index 00000000..e4a7f26c
--- /dev/null
+++ b/apps/backend/app/api/routes/whois/services.py
@@ -0,0 +1,193 @@
+from __future__ import annotations
+
+import ipaddress
+import re
+from typing import Any
+
+import httpx
+from fastapi import HTTPException
+
+from app.api.routes.whois.schema import WhoisContact, WhoisResult
+from app.core.cache import cached
+from app.core.cache.keys import register_namespace
+
+# Global cache: RDAP data is public and slow-moving. Registered here (not in
+# keys.py) so the whois module stays self-contained.
+register_namespace("whois", scope="global", default_ttl=3600)
+
+RDAP_BASE_URL = "https://rdap.org"
+_HTTP_TIMEOUT_S = 15.0
+
+_DOMAIN_RE = re.compile(r"^(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$")
+
+# RDAP eventAction -> normalized field
+_EVENT_FIELD = {
+ "registration": "created",
+ "last changed": "updated",
+ "last update": "updated",
+ "expiration": "expires",
+}
+
+
+def sanitize_target(raw: str) -> tuple[str, str]:
+ """Sanitize and classify a lookup target.
+
+ Strips scheme/path/port noise, lowercases, and validates. Returns
+ ``(target, kind)`` where kind is ``"domain"`` or ``"ip"``.
+ Raises ``HTTPException(400)`` for anything that is neither.
+ """
+ target = raw.strip()
+ target = re.sub(r"^[a-zA-Z][a-zA-Z0-9+.\-]*://", "", target) # scheme
+ target = target.split("/", 1)[0].split("?", 1)[0].split("#", 1)[0] # path/query
+ target = target.strip().strip(".").lower()
+ if target.startswith("[") and target.endswith("]"): # bracketed IPv6
+ target = target[1:-1]
+
+ if not target or len(target) > 253:
+ raise HTTPException(status_code=400, detail="Invalid domain or IP address")
+
+ try:
+ ipaddress.ip_address(target)
+ return target, "ip"
+ except ValueError:
+ pass
+
+ # Not an IP — strip a :port suffix if present, then validate as a domain.
+ host = target.rsplit(":", 1)[0] if re.search(r":\d+$", target) else target
+ if _is_valid_domain(host):
+ return host, "domain"
+
+ raise HTTPException(status_code=400, detail="Invalid domain or IP address")
+
+
+def _is_valid_domain(domain: str) -> bool:
+ return bool(_DOMAIN_RE.match(domain)) and len(domain) <= 253
+
+
+@cached(ns="whois", ttl=3600, scope="global")
+async def fetch_rdap(*, kind: str, target: str) -> dict[str, Any] | None:
+ """Fetch the raw RDAP document for a domain or IP.
+
+ Pure function of (kind, target) — cached globally for 1h.
+ Returns ``None`` when the registry has no record (RDAP 404).
+ Raises ``HTTPException`` for upstream failures (not cached).
+ """
+ path = "domain" if kind == "domain" else "ip"
+ url = f"{RDAP_BASE_URL}/{path}/{target}"
+ try:
+ async with httpx.AsyncClient(timeout=_HTTP_TIMEOUT_S, follow_redirects=True) as client:
+ resp = await client.get(url, headers={"accept": "application/rdap+json, application/json"})
+ except httpx.HTTPError:
+ raise HTTPException(status_code=502, detail="RDAP lookup failed: upstream service unreachable")
+
+ if resp.status_code == 404:
+ return None
+ if resp.status_code == 429:
+ raise HTTPException(status_code=503, detail="RDAP service is rate limiting requests; try again shortly")
+ if resp.status_code >= 400:
+ raise HTTPException(status_code=502, detail=f"RDAP upstream error (HTTP {resp.status_code})")
+
+ try:
+ data = resp.json()
+ except ValueError:
+ raise HTTPException(status_code=502, detail="RDAP upstream returned an invalid response")
+ if not isinstance(data, dict):
+ raise HTTPException(status_code=502, detail="RDAP upstream returned an invalid response")
+ return data
+
+
+def _vcard_value(entity: dict[str, Any], key: str) -> str | None:
+ """Extract a text value (fn, org, …) from an RDAP entity's vcardArray."""
+ vcard = entity.get("vcardArray")
+ if not isinstance(vcard, list) or len(vcard) < 2 or not isinstance(vcard[1], list):
+ return None
+ for item in vcard[1]:
+ if isinstance(item, list) and len(item) >= 4 and item[0] == key:
+ value = item[3]
+ if isinstance(value, list):
+ value = " ".join(str(v) for v in value if v)
+ text = str(value).strip()
+ if text:
+ return text
+ return None
+
+
+def _collect_contacts(entities: Any, out: list[WhoisContact], depth: int = 0) -> None:
+ if depth > 3 or not isinstance(entities, list):
+ return
+ for entity in entities:
+ if not isinstance(entity, dict):
+ continue
+ roles = [str(r) for r in entity.get("roles", []) if r]
+ name = _vcard_value(entity, "fn")
+ org = _vcard_value(entity, "org")
+ handle = entity.get("handle")
+ handle = str(handle) if handle else None
+ # Respect redaction: keep whatever the registry exposes; skip entries
+ # that carry no usable information at all.
+ if roles or name or org or handle:
+ out.append(WhoisContact(roles=roles, name=name, organization=org, handle=handle))
+ _collect_contacts(entity.get("entities"), out, depth + 1)
+
+
+def _find_registrar(data: dict[str, Any]) -> str | None:
+ for entity in data.get("entities") or []:
+ if not isinstance(entity, dict):
+ continue
+ if "registrar" in (entity.get("roles") or []):
+ return _vcard_value(entity, "fn") or _vcard_value(entity, "org") or entity.get("handle")
+ return None
+
+
+def _extract_events(data: dict[str, Any]) -> dict[str, str | None]:
+ out: dict[str, str | None] = {"created": None, "updated": None, "expires": None}
+ for event in data.get("events") or []:
+ if not isinstance(event, dict):
+ continue
+ action = str(event.get("eventAction", "")).lower()
+ field = _EVENT_FIELD.get(action)
+ if field and event.get("eventDate") and out[field] is None:
+ out[field] = str(event["eventDate"])
+ return out
+
+
+def normalize_rdap(target: str, kind: str, data: dict[str, Any]) -> WhoisResult:
+ events = _extract_events(data)
+
+ nameservers: list[str] = []
+ for ns in data.get("nameservers") or []:
+ if isinstance(ns, dict) and ns.get("ldhName"):
+ name = str(ns["ldhName"]).rstrip(".").lower()
+ if name and name not in nameservers:
+ nameservers.append(name)
+
+ contacts: list[WhoisContact] = []
+ _collect_contacts(data.get("entities"), contacts)
+
+ registrar = _find_registrar(data)
+ if registrar is None and kind == "ip":
+ # For IP networks, surface the network name/handle as the "registrar" line.
+ registrar = data.get("name") or data.get("handle")
+ registrar = str(registrar) if registrar else None
+
+ return WhoisResult(
+ target=target,
+ type=kind, # type: ignore[arg-type]
+ registrar=registrar,
+ status=[str(s) for s in data.get("status") or []],
+ created=events["created"],
+ updated=events["updated"],
+ expires=events["expires"],
+ nameservers=nameservers,
+ contacts=contacts,
+ raw=data,
+ )
+
+
+async def lookup_whois(raw_target: str) -> WhoisResult:
+ target, kind = sanitize_target(raw_target)
+ data = await fetch_rdap(kind=kind, target=target)
+ if data is None:
+ label = "domain" if kind == "domain" else "IP address"
+ raise HTTPException(status_code=404, detail=f"No RDAP/WHOIS record found for this {label}")
+ return normalize_rdap(target, kind, data)
diff --git a/apps/backend/app/api/routes/workspaces/api.py b/apps/backend/app/api/routes/workspaces/api.py
index 085c0638..0c58a8c7 100644
--- a/apps/backend/app/api/routes/workspaces/api.py
+++ b/apps/backend/app/api/routes/workspaces/api.py
@@ -1,43 +1,23 @@
from typing import Annotated
-from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
+from fastapi import APIRouter, Depends, HTTPException, Response, status
from app.api.routes.auth.services import get_current_uid
-from app.api.routes.workspaces import crud_service
from app.api.routes.workspaces.middleware import ACTIVE_WS_COOKIE
-from app.api.routes.workspaces import repo
from app.api.routes.workspaces.repo import (
- find_user_orgs, find_user_workspaces, find_workspace, find_ws_membership,
- find_org_membership, find_workspace_members,
+ find_user_workspaces, find_workspace, find_ws_membership,
)
-from app.api.routes.workspaces import members_service
-from app.api.routes.workspaces import invitations_service
from app.api.routes.workspaces.schema import (
- ChangeRoleRequest, DekWrapOut, DekWrapPostRequest, EncryptionBlob, InvitationOut,
- InviteMemberRequest, KeypairOut, KeypairPostRequest, MemberOut, OrgCreate, OrgOut, OrgPatch,
- PendingWrapOut, RotateDekRequest, SetActiveWorkspaceRequest, SetActiveWorkspaceResponse,
- WorkspaceCreate, WorkspacePatch, WorkspaceOut, WrappedDekBlob,
+ EncryptionBlob, KeypairOut, KeypairPostRequest, SetActiveWorkspaceRequest,
+ SetActiveWorkspaceResponse, WorkspaceOut,
)
from app.api.routes.workspaces import crypto_repo
from app.core.config import get_settings
-from app.database import db_manager
-from app.utils.collection_name import USERS
router = APIRouter(prefix="/workspaces-api", tags=["workspaces"])
-def _org_to_out(org: dict) -> OrgOut:
- return OrgOut(
- id=org["_id"],
- name=org["name"],
- slug=org["slug"],
- kind=org["kind"],
- org_role=org["org_role"],
- )
-
-
def _ws_to_out(ws: dict) -> WorkspaceOut:
return WorkspaceOut(
id=ws["_id"],
- org_id=ws["org_id"],
name=ws["name"],
slug=ws["slug"],
is_personal=bool(ws.get("is_personal")),
@@ -47,36 +27,11 @@ def _ws_to_out(ws: dict) -> WorkspaceOut:
)
-@router.get("/orgs", response_model=list[OrgOut])
-async def list_orgs(uid: Annotated[str, Depends(get_current_uid)]) -> list[OrgOut]:
- orgs = await find_user_orgs(uid)
- return [_org_to_out(o) for o in orgs]
-
-
@router.get("/workspaces", response_model=list[WorkspaceOut])
async def list_workspaces(
uid: Annotated[str, Depends(get_current_uid)],
- org_id: str | None = Query(default=None),
) -> list[WorkspaceOut]:
- workspaces = await find_user_workspaces(uid, org_id=org_id)
- # Auto-heal: any org the user belongs to but has no workspaces in (e.g.
- # accepted an org-only invite under an older build) gets a Personal
- # workspace seeded so the org-switcher has something to land on.
- from app.api.routes.workspaces.repo import (
- find_user_orgs,
- upsert_personal_workspace,
- upsert_ws_membership,
- )
- orgs_with_ws = {w["org_id"] for w in workspaces}
- user_orgs = await find_user_orgs(uid)
- missing = [o for o in user_orgs if o["_id"] not in orgs_with_ws]
- if org_id is not None:
- missing = [o for o in missing if o["_id"] == org_id]
- if missing:
- for org in missing:
- ws_id = await upsert_personal_workspace(org["_id"], uid)
- await upsert_ws_membership(ws_id, org["_id"], uid, "admin")
- workspaces = await find_user_workspaces(uid, org_id=org_id)
+ workspaces = await find_user_workspaces(uid)
return [_ws_to_out(w) for w in workspaces]
@@ -116,150 +71,6 @@ async def set_active_workspace(
return SetActiveWorkspaceResponse(workspace_id=body.workspace_id)
-@router.post("/orgs", response_model=OrgOut, status_code=201)
-async def create_org_route(
- body: OrgCreate,
- uid: Annotated[str, Depends(get_current_uid)],
-) -> OrgOut:
- return await crud_service.create_org(uid, body)
-
-
-@router.patch("/orgs/{org_id}", response_model=OrgOut)
-async def rename_org_route(
- org_id: str,
- body: OrgPatch,
- uid: Annotated[str, Depends(get_current_uid)],
-) -> OrgOut:
- return await crud_service.rename_org(uid, org_id, body)
-
-
-@router.delete("/orgs/{org_id}", status_code=204)
-async def delete_org_route(
- org_id: str,
- uid: Annotated[str, Depends(get_current_uid)],
-) -> None:
- await crud_service.delete_org(uid, org_id)
-
-
-@router.post(
- "/orgs/{org_id}/workspaces",
- response_model=WorkspaceOut,
- status_code=201,
-)
-async def create_workspace_route(
- org_id: str,
- body: WorkspaceCreate,
- uid: Annotated[str, Depends(get_current_uid)],
-) -> WorkspaceOut:
- return await crud_service.create_shared_workspace(uid, org_id, body)
-
-
-@router.patch("/workspaces/{ws_id}", response_model=WorkspaceOut)
-async def rename_workspace_route(
- ws_id: str,
- body: WorkspacePatch,
- uid: Annotated[str, Depends(get_current_uid)],
-) -> WorkspaceOut:
- return await crud_service.rename_workspace(uid, ws_id, body)
-
-
-@router.delete("/workspaces/{ws_id}", status_code=204)
-async def delete_workspace_route(
- ws_id: str,
- uid: Annotated[str, Depends(get_current_uid)],
-) -> None:
- await crud_service.delete_workspace(uid, ws_id)
-
-
-@router.get("/orgs/{org_id}/members", response_model=list[MemberOut])
-async def list_org_members_route(
- org_id: str, uid: Annotated[str, Depends(get_current_uid)],
-) -> list[MemberOut]:
- return await members_service.list_org_members(uid, org_id)
-
-
-@router.patch("/orgs/{org_id}/members/{target_uid}", response_model=MemberOut)
-async def change_org_role_route(
- org_id: str, target_uid: str, body: ChangeRoleRequest,
- uid: Annotated[str, Depends(get_current_uid)],
-) -> MemberOut:
- return await members_service.change_org_role(uid, org_id, target_uid, body.role)
-
-
-@router.delete("/orgs/{org_id}/members/{target_uid}", status_code=204)
-async def remove_org_member_route(
- org_id: str, target_uid: str,
- uid: Annotated[str, Depends(get_current_uid)],
-) -> None:
- await members_service.remove_org_member(uid, org_id, target_uid)
-
-
-@router.get("/workspaces/{ws_id}/members", response_model=list[MemberOut])
-async def list_workspace_members_route(
- ws_id: str, uid: Annotated[str, Depends(get_current_uid)],
-) -> list[MemberOut]:
- return await members_service.list_workspace_members(uid, ws_id)
-
-
-@router.patch("/workspaces/{ws_id}/members/{target_uid}", response_model=MemberOut)
-async def change_workspace_role_route(
- ws_id: str, target_uid: str, body: ChangeRoleRequest,
- uid: Annotated[str, Depends(get_current_uid)],
-) -> MemberOut:
- return await members_service.change_workspace_role(uid, ws_id, target_uid, body.role)
-
-
-@router.delete("/workspaces/{ws_id}/members/{target_uid}", status_code=204)
-async def remove_workspace_member_route(
- ws_id: str, target_uid: str,
- uid: Annotated[str, Depends(get_current_uid)],
-) -> None:
- await members_service.remove_workspace_member(uid, ws_id, target_uid)
-
-
-@router.post(
- "/orgs/{org_id}/members",
- response_model=InvitationOut, status_code=201,
-)
-async def invite_to_org_route(
- org_id: str, body: InviteMemberRequest,
- uid: Annotated[str, Depends(get_current_uid)],
-) -> InvitationOut:
- return await invitations_service.invite_to_org(uid, org_id, body)
-
-
-@router.post(
- "/workspaces/{ws_id}/members",
- response_model=InvitationOut, status_code=201,
-)
-async def invite_to_workspace_route(
- ws_id: str, body: InviteMemberRequest,
- uid: Annotated[str, Depends(get_current_uid)],
-) -> InvitationOut:
- return await invitations_service.invite_to_workspace(uid, ws_id, body)
-
-
-@router.get("/invitations/pending", response_model=list[InvitationOut])
-async def list_pending_invitations_route(
- uid: Annotated[str, Depends(get_current_uid)],
-) -> list[InvitationOut]:
- return await invitations_service.list_pending_for_me(uid)
-
-
-@router.post("/invitations/{token}/accept")
-async def accept_invitation_route(
- token: str, uid: Annotated[str, Depends(get_current_uid)],
-) -> dict:
- return await invitations_service.accept_invitation(uid, token)
-
-
-@router.post("/invitations/{token}/revoke", status_code=204)
-async def revoke_invitation_route(
- token: str, uid: Annotated[str, Depends(get_current_uid)],
-) -> None:
- await invitations_service.revoke_invitation(uid, token)
-
-
@router.get("/users/me/keypair", response_model=KeypairOut | None)
async def get_my_keypair(
uid: Annotated[str, Depends(get_current_uid)],
@@ -292,166 +103,3 @@ async def set_my_keypair(
},
salt=body.salt,
)
-
-
-@router.get("/workspaces/{ws_id}/dek-wrap", response_model=DekWrapOut)
-async def get_my_dek_wrap(
- ws_id: str,
- uid: Annotated[str, Depends(get_current_uid)],
-) -> DekWrapOut:
- ws = await find_workspace(ws_id)
- if not ws:
- raise HTTPException(404)
- mem = await find_ws_membership(ws_id, uid)
- if not mem:
- raise HTTPException(403)
- wrap = mem.get("wrappedDek")
- enc = (ws.get("settings") or {}).get("encryption") or {}
- return DekWrapOut(
- wrappedDek=WrappedDekBlob(**wrap) if wrap else None,
- wrappedDekVersion=mem.get("wrappedDekVersion", 0),
- expectedFingerprint=enc.get("dekFingerprint"),
- )
-
-
-@router.post("/workspaces/{ws_id}/dek-wrap", status_code=204)
-async def post_dek_wrap_for_member(
- ws_id: str,
- body: DekWrapPostRequest,
- uid: Annotated[str, Depends(get_current_uid)],
-) -> None:
- ws = await find_workspace(ws_id)
- if not ws:
- raise HTTPException(404)
- caller_mem = await find_ws_membership(ws_id, uid)
- org_mem = await find_org_membership(ws["org_id"], uid) if ws.get("org_id") else None
- is_admin = (caller_mem and caller_mem["ws_role"] == "admin") or (org_mem and org_mem["org_role"] in ("owner", "admin"))
- if not is_admin:
- raise HTTPException(403)
- # L-1: only wrap for an actual member. set_membership_wrapped_dek uses a
- # non-upsert update, so a wrap for a non-member would silently no-op and
- # hide the caller's mistake. Reject it explicitly instead.
- target_mem = await find_ws_membership(ws_id, body.target_uid)
- if not target_mem:
- raise HTTPException(404, "Target user is not a workspace member")
- existing = await crypto_repo.get_membership_wrap(ws_id, body.target_uid)
- new_version = (existing["wrappedDekVersion"] if existing else 0) + 1
- await crypto_repo.set_membership_wrapped_dek(
- ws_id,
- body.target_uid,
- wrapped={
- "encrypted": body.wrapped.encrypted,
- "iv": body.wrapped.iv,
- "senderPublicKey": body.wrapped.senderPublicKey,
- },
- version=new_version,
- )
-
-
-@router.post("/workspaces/{ws_id}/rotate-dek", status_code=204)
-async def rotate_dek(
- ws_id: str,
- body: RotateDekRequest,
- uid: Annotated[str, Depends(get_current_uid)],
-) -> None:
- from app.utils.utils import create_timestamp
- ws = await find_workspace(ws_id)
- if not ws:
- raise HTTPException(404)
- caller_mem = await find_ws_membership(ws_id, uid)
- org_mem = await find_org_membership(ws["org_id"], uid) if ws.get("org_id") else None
- is_admin = (caller_mem and caller_mem["ws_role"] == "admin") or (org_mem and org_mem["org_role"] in ("owner", "admin"))
- if not is_admin:
- raise HTTPException(403)
- members = await find_workspace_members(ws_id)
- member_uids = {m["uid"] for m in members}
- submitted_uids = {w.uid for w in body.wraps}
- if submitted_uids != member_uids:
- raise HTTPException(400, "Wraps must cover all current members exactly")
- max_existing = max([m.get("wrappedDekVersion", 0) for m in members] + [0])
- new_version = max_existing + 1
- await crypto_repo.bulk_set_wrapped_deks(
- ws_id,
- [
- {
- "uid": w.uid,
- "wrapped": {
- "encrypted": w.wrapped.encrypted,
- "iv": w.wrapped.iv,
- "senderPublicKey": w.wrapped.senderPublicKey,
- },
- "version": new_version,
- }
- for w in body.wraps
- ],
- )
- await crypto_repo.set_workspace_encryption(
- ws_id,
- scheme="shared-dek-v1",
- dek_fingerprint=body.dekFingerprint,
- rotated_at=create_timestamp(),
- )
-
-
-@router.get("/workspaces/{ws_id}/member-publickeys", response_model=list[PendingWrapOut])
-async def list_member_publickeys(
- ws_id: str,
- uid: Annotated[str, Depends(get_current_uid)],
-) -> list[PendingWrapOut]:
- ws = await find_workspace(ws_id)
- if not ws:
- raise HTTPException(404)
- caller_mem = await find_ws_membership(ws_id, uid)
- org_mem = await find_org_membership(ws["org_id"], uid) if ws.get("org_id") else None
- is_admin = (
- (caller_mem and caller_mem["ws_role"] == "admin")
- or (org_mem and org_mem["org_role"] in ("owner", "admin"))
- )
- if not is_admin:
- raise HTTPException(403)
- members = await find_workspace_members(ws_id)
- if not members:
- return []
- member_uids = [m["uid"] for m in members]
- users = await db_manager.find(USERS, {"_id": {"$in": member_uids}}, limit=500)
- by_uid = {u["_id"]: u for u in users}
- return [
- PendingWrapOut(
- uid=m["uid"],
- email=by_uid.get(m["uid"], {}).get("email"),
- publicKey=(by_uid.get(m["uid"], {}).get("encryption") or {}).get("publicKey"),
- )
- for m in members
- ]
-
-
-@router.get("/workspaces/{ws_id}/pending-wraps", response_model=list[PendingWrapOut])
-async def list_pending_wraps(
- ws_id: str,
- uid: Annotated[str, Depends(get_current_uid)],
-) -> list[PendingWrapOut]:
- ws = await find_workspace(ws_id)
- if not ws:
- raise HTTPException(404)
- caller_mem = await find_ws_membership(ws_id, uid)
- org_mem = await find_org_membership(ws["org_id"], uid) if ws.get("org_id") else None
- is_admin = (
- (caller_mem and caller_mem["ws_role"] == "admin")
- or (org_mem and org_mem["org_role"] in ("owner", "admin"))
- )
- if not is_admin:
- raise HTTPException(403)
- pendings = await crypto_repo.find_pending_wraps(ws_id)
- if not pendings:
- return []
- uids = [p["uid"] for p in pendings]
- users = await db_manager.find(USERS, {"_id": {"$in": uids}}, limit=200)
- by_uid = {u["_id"]: u for u in users}
- return [
- PendingWrapOut(
- uid=p["uid"],
- email=by_uid.get(p["uid"], {}).get("email"),
- publicKey=(by_uid.get(p["uid"], {}).get("encryption") or {}).get("publicKey"),
- )
- for p in pendings
- ]
diff --git a/apps/backend/app/api/routes/workspaces/backfill.py b/apps/backend/app/api/routes/workspaces/backfill.py
index f6094007..8aaea488 100644
--- a/apps/backend/app/api/routes/workspaces/backfill.py
+++ b/apps/backend/app/api/routes/workspaces/backfill.py
@@ -63,8 +63,8 @@ class BackfillSpec:
]
-async def _stamp_collection(spec: BackfillSpec, uid: str, ws_id: str, org_id: str) -> int:
- update: dict[str, Any] = {"org_id": org_id, "workspace_id": ws_id}
+async def _stamp_collection(spec: BackfillSpec, uid: str, ws_id: str) -> int:
+ update: dict[str, Any] = {"workspace_id": ws_id}
if spec.stamp_owner_uid:
update["owner_uid"] = uid
res = await db_manager.update_many(
@@ -94,7 +94,7 @@ async def _rewrite_pinned_tools(uid: str, ws_id: str) -> None:
)
-async def run_user_backfill(uid: str, personal_workspace_id: str, org_id: str) -> None:
+async def run_user_backfill(uid: str, personal_workspace_id: str) -> None:
# Skip if already migrated
migrated_at = await users_repo.get_migrated_at(uid)
if migrated_at:
@@ -104,7 +104,7 @@ async def run_user_backfill(uid: str, personal_workspace_id: str, org_id: str) -
for spec in BACKFILL_COLLECTIONS:
if progress.get(spec.collection) == "done":
continue
- await _stamp_collection(spec, uid, personal_workspace_id, org_id)
+ await _stamp_collection(spec, uid, personal_workspace_id)
progress[spec.collection] = "done"
await users_repo.set_migration_progress(uid, progress)
@@ -116,6 +116,5 @@ def schedule_backfill(
background_tasks: BackgroundTasks,
uid: str,
personal_workspace_id: str,
- org_id: str,
) -> None:
- background_tasks.add_task(run_user_backfill, uid, personal_workspace_id, org_id)
+ background_tasks.add_task(run_user_backfill, uid, personal_workspace_id)
diff --git a/apps/backend/app/api/routes/workspaces/crud_service.py b/apps/backend/app/api/routes/workspaces/crud_service.py
deleted file mode 100644
index cd9b40e0..00000000
--- a/apps/backend/app/api/routes/workspaces/crud_service.py
+++ /dev/null
@@ -1,160 +0,0 @@
-import re
-from fastapi import HTTPException
-from app.api.routes.workspaces import repo
-from app.api.routes.workspaces.schema import OrgCreate, OrgOut, OrgPatch, WorkspaceCreate, WorkspacePatch, WorkspaceOut
-from app.database import db_manager
-from app.utils.collection_name import ORGANIZATIONS, WORKSPACES
-from app.utils.utils import create_timestamp, new_id
-
-
-def _slugify(name: str) -> str:
- slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
- return slug or "untitled"
-
-
-async def _org_to_out(org: dict, uid: str) -> OrgOut:
- mem = await repo.find_org_membership(org["_id"], uid)
- return OrgOut(
- id=org["_id"],
- name=org["name"],
- slug=org["slug"],
- kind=org["kind"],
- org_role=mem["org_role"] if mem else "viewer",
- )
-
-
-async def _require_org(org_id: str) -> dict:
- org = await repo.find_org(org_id)
- if not org:
- raise HTTPException(status_code=404, detail="Org not found")
- return org
-
-
-async def create_org(uid: str, body: OrgCreate) -> OrgOut:
- slug = body.slug or _slugify(body.name)
- org_id = await repo.upsert_org(
- name=body.name,
- slug=slug,
- kind="user",
- owner_uid=uid,
- )
- await repo.upsert_org_membership(org_id, uid, "owner")
- org = await _require_org(org_id)
- return await _org_to_out(org, uid)
-
-
-async def rename_org(uid: str, org_id: str, body: OrgPatch) -> OrgOut:
- mem = await repo.find_org_membership(org_id, uid)
- if not mem or mem["org_role"] not in ("owner", "admin"):
- raise HTTPException(status_code=403, detail="Org admin required")
- org = await _require_org(org_id)
- if org["kind"] == "system":
- raise HTTPException(status_code=403, detail="System org cannot be modified")
- if body.name and body.name != org["name"]:
- await db_manager.update_one(
- ORGANIZATIONS,
- {"_id": org_id},
- {"$set": {"name": body.name, "updatedAt": create_timestamp()}},
- )
- org = await _require_org(org_id)
- return await _org_to_out(org, uid)
-
-
-async def delete_org(uid: str, org_id: str) -> None:
- mem = await repo.find_org_membership(org_id, uid)
- if not mem or mem["org_role"] != "owner":
- raise HTTPException(status_code=403, detail="Org owner required")
- org = await _require_org(org_id)
- if org["kind"] == "system":
- raise HTTPException(status_code=403, detail="System org cannot be deleted")
- ts = create_timestamp()
- await repo.set_org_deleted(org_id, ts)
- # Cascade soft-delete on all non-deleted workspaces in the org.
- await db_manager.update_many(
- WORKSPACES,
- {"org_id": org_id, "deleted_at": None},
- {"$set": {"deleted_at": ts}},
- )
-
-
-async def _ws_to_out(ws: dict, ws_role: str) -> WorkspaceOut:
- return WorkspaceOut(
- id=ws["_id"],
- org_id=ws["org_id"],
- name=ws["name"],
- slug=ws["slug"],
- is_personal=bool(ws.get("is_personal")),
- kind=ws.get("kind", "shared"),
- ws_role=ws_role,
- settings=ws.get("settings") or {},
- )
-
-
-async def create_shared_workspace(
- uid: str, org_id: str, body: WorkspaceCreate,
-) -> WorkspaceOut:
- org = await repo.find_org(org_id)
- if not org:
- raise HTTPException(status_code=404, detail="Org not found")
- org_mem = await repo.find_org_membership(org_id, uid)
- if not org_mem:
- raise HTTPException(status_code=403, detail="Not a member of this org")
- # System orgs (Mydevtools Cloud) let any member create their own workspaces —
- # listings are per-user, so workspaces here don't leak across the tenant.
- is_system = org.get("kind") == "system"
- if not is_system and org_mem["org_role"] not in ("owner", "admin"):
- raise HTTPException(status_code=403, detail="Org admin required")
- slug = body.slug or _slugify(body.name)
- ts = create_timestamp()
- doc = {
- "_id": new_id(),
- "org_id": org_id,
- "name": body.name,
- "slug": slug,
- "is_personal": False,
- "owner_uid": None,
- "kind": "shared",
- "settings": {"encryption": None},
- "createdAt": ts,
- "updatedAt": ts,
- "deleted_at": None,
- }
- await db_manager.insert_one(WORKSPACES, doc)
- # Create explicit ws membership so the listing endpoint surfaces the workspace.
- await repo.upsert_ws_membership(doc["_id"], org_id, uid, "admin")
- return await _ws_to_out(doc, "admin")
-
-
-async def rename_workspace(
- uid: str, ws_id: str, body: WorkspacePatch,
-) -> WorkspaceOut:
- ws = await repo.find_workspace(ws_id)
- if not ws:
- raise HTTPException(status_code=404, detail="Workspace not found")
- if ws.get("is_personal"):
- raise HTTPException(status_code=403, detail="Personal workspace is locked")
- org_mem = await repo.find_org_membership(ws["org_id"], uid)
- if not org_mem or org_mem["org_role"] not in ("owner", "admin"):
- ws_mem = await repo.find_ws_membership(ws_id, uid)
- if not ws_mem or ws_mem["ws_role"] != "admin":
- raise HTTPException(status_code=403, detail="Workspace admin required")
- if body.name and body.name != ws["name"]:
- await db_manager.update_one(
- WORKSPACES, {"_id": ws_id},
- {"$set": {"name": body.name, "updatedAt": create_timestamp()}},
- )
- ws = await repo.find_workspace(ws_id)
- mem = await repo.find_ws_membership(ws_id, uid)
- return await _ws_to_out(ws, (mem or {"ws_role": "admin"})["ws_role"])
-
-
-async def delete_workspace(uid: str, ws_id: str) -> None:
- ws = await repo.find_workspace(ws_id)
- if not ws:
- raise HTTPException(status_code=404, detail="Workspace not found")
- if ws.get("is_personal"):
- raise HTTPException(status_code=403, detail="Personal workspace is locked")
- org_mem = await repo.find_org_membership(ws["org_id"], uid)
- if not org_mem or org_mem["org_role"] not in ("owner", "admin"):
- raise HTTPException(status_code=403, detail="Org admin required")
- await repo.set_workspace_deleted(ws_id, create_timestamp())
diff --git a/apps/backend/app/api/routes/workspaces/crypto_repo.py b/apps/backend/app/api/routes/workspaces/crypto_repo.py
index 350f089a..5cf241f2 100644
--- a/apps/backend/app/api/routes/workspaces/crypto_repo.py
+++ b/apps/backend/app/api/routes/workspaces/crypto_repo.py
@@ -1,6 +1,6 @@
from typing import Any
from app.database import db_manager
-from app.utils.collection_name import USERS, WORKSPACES, WORKSPACE_MEMBERSHIPS
+from app.utils.collection_name import USERS
from app.utils.utils import create_timestamp
@@ -31,114 +31,3 @@ async def get_user_encryption(uid: str) -> dict[str, Any] | None:
if not doc:
return None
return doc.get("encryption")
-
-
-async def set_membership_wrapped_dek(
- workspace_id: str,
- uid: str,
- wrapped: dict[str, str], # {encrypted, iv, senderPublicKey}
- version: int,
-) -> None:
- await db_manager.update_one(
- WORKSPACE_MEMBERSHIPS,
- {"workspace_id": workspace_id, "uid": uid},
- {"$set": {
- "wrappedDek": wrapped,
- "wrappedDekVersion": version,
- }},
- )
-
-
-async def get_membership_wrap(workspace_id: str, uid: str) -> dict | None:
- doc = await db_manager.find_one(
- WORKSPACE_MEMBERSHIPS,
- {"workspace_id": workspace_id, "uid": uid},
- )
- if not doc:
- return None
- return {
- "wrappedDek": doc.get("wrappedDek"),
- "wrappedDekVersion": doc.get("wrappedDekVersion", 0),
- }
-
-
-async def set_workspace_encryption(
- workspace_id: str,
- *,
- scheme: str,
- dek_fingerprint: str,
- rotated_at: int | None = None,
-) -> None:
- ws = await db_manager.find_one(WORKSPACES, {"_id": workspace_id})
- existing_enc = (ws.get("settings") or {}).get("encryption") if ws else None
- created_at = (existing_enc or {}).get("createdAt") or create_timestamp()
- await db_manager.update_one(
- WORKSPACES,
- {"_id": workspace_id},
- {"$set": {"settings.encryption": {
- "scheme": scheme,
- "dekFingerprint": dek_fingerprint,
- "createdAt": created_at,
- "rotatedAt": rotated_at,
- }}},
- )
-
-
-async def flag_workspace_rotation_required(workspace_id: str) -> bool:
- """Mark an encrypted workspace as needing a DEK rotation (e.g. after a member
- is removed). No-op if the workspace has no encryption initialized. Returns
- True if the flag was set. The flag is cleared on the next rotation because
- set_workspace_encryption() rewrites settings.encryption without it.
- """
- ws = await db_manager.find_one(WORKSPACES, {"_id": workspace_id})
- enc = (ws.get("settings") or {}).get("encryption") if ws else None
- if not enc:
- return False
- await db_manager.update_one(
- WORKSPACES,
- {"_id": workspace_id},
- {"$set": {"settings.encryption.rotationRequired": True}},
- )
- return True
-
-
-async def bulk_set_wrapped_deks(
- workspace_id: str,
- wraps: list[dict], # [{uid, wrapped, version}, ...]
-) -> None:
- for w in wraps:
- await set_membership_wrapped_dek(
- workspace_id, w["uid"], w["wrapped"], w["version"],
- )
-
-
-async def find_pending_wraps(workspace_id: str) -> list[dict]:
- docs = await db_manager.find(
- WORKSPACE_MEMBERSHIPS,
- {
- "workspace_id": workspace_id,
- "$or": [
- {"wrappedDek": None},
- {"wrappedDek": {"$exists": False}},
- ],
- },
- limit=200,
- )
- return docs
-
-
-async def find_users_with_publickey_by_emails(emails: list[str]) -> dict[str, dict]:
- """Return uid -> {publicKey, email} for any registered user with a published keypair."""
- docs = await db_manager.find(
- USERS,
- {"email": {"$in": [e.lower() for e in emails]}, "encryption.publicKey": {"$exists": True}},
- limit=500,
- )
- return {
- d["_id"]: {
- "publicKey": d["encryption"]["publicKey"],
- "email": d["email"],
- }
- for d in docs
- if d.get("encryption") and d["encryption"].get("publicKey")
- }
diff --git a/apps/backend/app/api/routes/workspaces/invitations_repo.py b/apps/backend/app/api/routes/workspaces/invitations_repo.py
deleted file mode 100644
index 3ab731eb..00000000
--- a/apps/backend/app/api/routes/workspaces/invitations_repo.py
+++ /dev/null
@@ -1,42 +0,0 @@
-from typing import Any
-from app.database import db_manager
-from app.utils.collection_name import INVITATIONS
-
-
-async def create_invitation(doc: dict[str, Any]) -> str:
- await db_manager.insert_one(INVITATIONS, doc)
- return doc["_id"]
-
-
-async def find_invitation_by_token(token: str) -> dict[str, Any] | None:
- return await db_manager.find_one(INVITATIONS, {"token": token})
-
-
-async def find_pending_for_email(email: str) -> list[dict[str, Any]]:
- return await db_manager.find(
- INVITATIONS,
- {"invited_email": email.lower(), "status": "pending"},
- limit=50,
- )
-
-
-async def find_pending_for_org(org_id: str) -> list[dict[str, Any]]:
- return await db_manager.find(
- INVITATIONS,
- {"org_id": org_id, "status": "pending"},
- limit=200,
- )
-
-
-async def update_invitation_status(
- invitation_id: str,
- status: str,
- accepted_uid: str | None = None,
- accepted_at: int | None = None,
-) -> None:
- patch: dict[str, Any] = {"status": status}
- if accepted_uid is not None:
- patch["accepted_uid"] = accepted_uid
- if accepted_at is not None:
- patch["accepted_at"] = accepted_at
- await db_manager.update_one(INVITATIONS, {"_id": invitation_id}, {"$set": patch})
diff --git a/apps/backend/app/api/routes/workspaces/invitations_service.py b/apps/backend/app/api/routes/workspaces/invitations_service.py
deleted file mode 100644
index ecff7fea..00000000
--- a/apps/backend/app/api/routes/workspaces/invitations_service.py
+++ /dev/null
@@ -1,191 +0,0 @@
-import secrets
-from fastapi import HTTPException
-
-from app.api.routes.auth.users_repo import get_user_doc
-from app.api.routes.workspaces import invitations_repo, repo
-from app.api.routes.workspaces.schema import InvitationOut, InviteMemberRequest
-from app.utils.utils import create_timestamp, new_id
-
-INVITATION_TTL_SECONDS = 14 * 24 * 3600
-
-
-def _new_token() -> str:
- return secrets.token_urlsafe(32)
-
-
-def _doc_to_out(doc: dict) -> InvitationOut:
- return InvitationOut(
- id=doc["_id"],
- org_id=doc["org_id"],
- workspace_id=doc.get("workspace_id"),
- invited_email=doc["invited_email"],
- invited_uid=doc.get("invited_uid"),
- invited_role_org=doc.get("invited_role_org"),
- invited_role_ws=doc.get("invited_role_ws"),
- status=doc["status"],
- token=doc["token"],
- expires_at=int(doc["expires_at"]),
- created_at=int(doc["created_at"]),
- )
-
-
-async def _find_uid_by_email(email: str) -> str | None:
- from app.database import db_manager
- from app.utils.collection_name import USERS
- doc = await db_manager.find_one(USERS, {"email": email.lower()})
- return doc["_id"] if doc else None
-
-
-async def _ensure_org_admin(uid: str, org_id: str) -> None:
- mem = await repo.find_org_membership(org_id, uid)
- if not mem or mem["org_role"] not in ("owner", "admin"):
- raise HTTPException(403, "Org admin required")
-
-
-async def _ensure_ws_admin(uid: str, ws_id: str) -> dict:
- ws = await repo.find_workspace(ws_id)
- if not ws:
- raise HTTPException(404, "Workspace not found")
- org_mem = await repo.find_org_membership(ws["org_id"], uid)
- ws_mem = await repo.find_ws_membership(ws_id, uid)
- if not (
- (org_mem and org_mem["org_role"] in ("owner", "admin"))
- or (ws_mem and ws_mem["ws_role"] == "admin")
- ):
- raise HTTPException(403, "Workspace admin required")
- return ws
-
-
-async def invite_to_org(
- uid: str, org_id: str, body: InviteMemberRequest,
-) -> InvitationOut:
- await _ensure_org_admin(uid, org_id)
- email = body.email.lower().strip()
- if body.role not in ("owner", "admin", "member", "viewer"):
- raise HTTPException(400, "Invalid org role")
- # Optional bundled workspace grant on accept.
- ws_id: str | None = body.workspace_id
- ws_role: str | None = body.workspace_role
- if ws_id:
- if ws_role not in ("admin", "developer", "viewer"):
- raise HTTPException(400, "Invalid workspace role")
- ws = await repo.find_workspace(ws_id)
- if not ws or ws.get("org_id") != org_id:
- raise HTTPException(400, "Workspace does not belong to this org")
- invited_uid = await _find_uid_by_email(email)
- if not invited_uid:
- raise HTTPException(404, "No user found with that email. Ask them to sign up first.")
- now = create_timestamp()
- doc = {
- "_id": new_id(),
- "org_id": org_id,
- "workspace_id": ws_id,
- "invited_email": email,
- "invited_uid": invited_uid,
- "invited_role_org": body.role,
- "invited_role_ws": ws_role,
- "token": _new_token(),
- "status": "pending",
- "invited_by": uid,
- "created_at": now,
- "expires_at": now + INVITATION_TTL_SECONDS * 1000,
- }
- await invitations_repo.create_invitation(doc)
- # In-app notification only — the invited user sees this via the bell.
- return _doc_to_out(doc)
-
-
-async def invite_to_workspace(
- uid: str, ws_id: str, body: InviteMemberRequest,
-) -> InvitationOut:
- ws = await _ensure_ws_admin(uid, ws_id)
- email = body.email.lower().strip()
- if body.role not in ("admin", "developer", "viewer"):
- raise HTTPException(400, "Invalid ws role")
- invited_uid = await _find_uid_by_email(email)
- if not invited_uid:
- raise HTTPException(404, "No user found with that email. Ask them to sign up first.")
- now = create_timestamp()
- doc = {
- "_id": new_id(),
- "org_id": ws["org_id"],
- "workspace_id": ws_id,
- "invited_email": email,
- "invited_uid": invited_uid,
- # Workspace-level invites also grant org Member by default.
- "invited_role_org": "member",
- "invited_role_ws": body.role,
- "token": _new_token(),
- "status": "pending",
- "invited_by": uid,
- "created_at": now,
- "expires_at": now + INVITATION_TTL_SECONDS * 1000,
- }
- await invitations_repo.create_invitation(doc)
- # In-app notification only — the invited user sees this via PendingInvitationsBadge.
- return _doc_to_out(doc)
-
-
-async def list_pending_for_me(uid: str) -> list[InvitationOut]:
- user = await get_user_doc(uid)
- if not user or not user.get("email"):
- return []
- docs = await invitations_repo.find_pending_for_email(user["email"])
- return [_doc_to_out(d) for d in docs]
-
-
-async def accept_invitation(uid: str, token: str) -> dict:
- inv = await invitations_repo.find_invitation_by_token(token)
- if not inv:
- raise HTTPException(404, "Invitation not found")
- user = await get_user_doc(uid)
- if not user or not user.get("email") or user["email"].lower() != inv["invited_email"]:
- raise HTTPException(403, "Invitation addressed to different email")
- if inv["status"] != "pending":
- raise HTTPException(400, f"Invitation is {inv['status']}")
- if inv["expires_at"] < create_timestamp():
- await invitations_repo.update_invitation_status(inv["_id"], "expired")
- raise HTTPException(400, "Invitation expired")
-
- # Apply memberships
- if inv["invited_role_org"]:
- existing = await repo.find_org_membership(inv["org_id"], uid)
- if not existing:
- await repo.upsert_org_membership(
- inv["org_id"], uid, inv["invited_role_org"],
- )
- if inv["workspace_id"] and inv["invited_role_ws"]:
- existing_ws = await repo.find_ws_membership(inv["workspace_id"], uid)
- if not existing_ws:
- await repo.upsert_ws_membership(
- inv["workspace_id"], inv["org_id"], uid, inv["invited_role_ws"],
- )
- else:
- # Org-only invite: seed a Personal workspace in this org so the user has
- # somewhere to land when they switch to it. Mirrors signup behavior.
- ws_id = await repo.upsert_personal_workspace(inv["org_id"], uid)
- await repo.upsert_ws_membership(ws_id, inv["org_id"], uid, "admin")
- await invitations_repo.update_invitation_status(
- inv["_id"], "accepted",
- accepted_uid=uid, accepted_at=create_timestamp(),
- )
- return {"org_id": inv["org_id"], "workspace_id": inv.get("workspace_id")}
-
-
-async def revoke_invitation(uid: str, token: str) -> None:
- inv = await invitations_repo.find_invitation_by_token(token)
- if not inv:
- raise HTTPException(404, "Invitation not found")
- # Allowed: original inviter, org owner/admin, OR the invitee themself (decline).
- user = await get_user_doc(uid)
- is_invitee = bool(
- (inv.get("invited_uid") and inv["invited_uid"] == uid)
- or (user and (user.get("email") or "").lower() == inv["invited_email"])
- )
- org_mem = await repo.find_org_membership(inv["org_id"], uid)
- is_admin = bool(org_mem and org_mem["org_role"] in ("owner", "admin"))
- if not (inv["invited_by"] == uid or is_admin or is_invitee):
- raise HTTPException(403, "Cannot revoke this invitation")
- if inv["status"] != "pending":
- return
- await invitations_repo.update_invitation_status(inv["_id"], "revoked")
diff --git a/apps/backend/app/api/routes/workspaces/members_service.py b/apps/backend/app/api/routes/workspaces/members_service.py
deleted file mode 100644
index 6829ff09..00000000
--- a/apps/backend/app/api/routes/workspaces/members_service.py
+++ /dev/null
@@ -1,156 +0,0 @@
-from fastapi import HTTPException
-from app.api.routes.auth.users_repo import get_user_doc
-from app.api.routes.workspaces import repo
-from app.api.routes.workspaces.schema import MemberOut
-from app.database import db_manager
-from app.utils.collection_name import (
- ORG_MEMBERSHIPS, WORKSPACE_MEMBERSHIPS, WORKSPACES,
-)
-from app.utils.utils import create_timestamp
-
-
-async def _membership_to_out(mem: dict, role_field: str) -> MemberOut:
- user = await get_user_doc(mem["uid"]) or {}
- return MemberOut(
- uid=mem["uid"],
- email=user.get("email"),
- display_name=user.get("display_name"),
- role=mem[role_field],
- since=int(mem.get("createdAt", 0)),
- )
-
-
-async def list_org_members(uid: str, org_id: str) -> list[MemberOut]:
- caller = await repo.find_org_membership(org_id, uid)
- if not caller:
- raise HTTPException(403, "Not an org member")
- members = await repo.find_org_members(org_id)
- return [await _membership_to_out(m, "org_role") for m in members]
-
-
-async def list_workspace_members(uid: str, ws_id: str) -> list[MemberOut]:
- ws = await repo.find_workspace(ws_id)
- if not ws:
- raise HTTPException(404, "Workspace not found")
- caller_org_mem = await repo.find_org_membership(ws["org_id"], uid)
- caller_ws_mem = await repo.find_ws_membership(ws_id, uid)
- if not caller_ws_mem and not (
- caller_org_mem and caller_org_mem["org_role"] in ("owner", "admin")
- ):
- raise HTTPException(403, "Not a workspace member")
- members = await repo.find_workspace_members(ws_id)
- return [await _membership_to_out(m, "ws_role") for m in members]
-
-
-def _validate_org_role(role: str) -> None:
- if role not in ("owner", "admin", "member", "viewer"):
- raise HTTPException(400, "Invalid org role")
-
-
-def _validate_ws_role(role: str) -> None:
- if role not in ("admin", "developer", "viewer"):
- raise HTTPException(400, "Invalid ws role")
-
-
-async def change_org_role(
- uid: str, org_id: str, target_uid: str, role: str,
-) -> MemberOut:
- _validate_org_role(role)
- caller = await repo.find_org_membership(org_id, uid)
- if not caller or caller["org_role"] not in ("owner", "admin"):
- raise HTTPException(403, "Org admin required")
- target = await repo.find_org_membership(org_id, target_uid)
- if not target:
- raise HTTPException(404, "Target not in org")
- if target["org_role"] == "owner" and role != "owner":
- owners = [
- m for m in await repo.find_org_members(org_id)
- if m["org_role"] == "owner"
- ]
- if len(owners) == 1:
- raise HTTPException(400, "Cannot demote sole owner")
- await db_manager.update_one(
- ORG_MEMBERSHIPS,
- {"org_id": org_id, "uid": target_uid},
- {"$set": {"org_role": role, "updatedAt": create_timestamp()}},
- )
- updated = await repo.find_org_membership(org_id, target_uid)
- return await _membership_to_out(updated, "org_role")
-
-
-async def change_workspace_role(
- uid: str, ws_id: str, target_uid: str, role: str,
-) -> MemberOut:
- _validate_ws_role(role)
- ws = await repo.find_workspace(ws_id)
- if not ws:
- raise HTTPException(404, "Workspace not found")
- caller_org_mem = await repo.find_org_membership(ws["org_id"], uid)
- caller_ws_mem = await repo.find_ws_membership(ws_id, uid)
- is_admin = (
- (caller_org_mem and caller_org_mem["org_role"] in ("owner", "admin"))
- or (caller_ws_mem and caller_ws_mem["ws_role"] == "admin")
- )
- if not is_admin:
- raise HTTPException(403, "Workspace admin required")
- target = await repo.find_ws_membership(ws_id, target_uid)
- if not target:
- raise HTTPException(404, "Target not in workspace")
- await db_manager.update_one(
- WORKSPACE_MEMBERSHIPS,
- {"workspace_id": ws_id, "uid": target_uid},
- {"$set": {"ws_role": role, "updatedAt": create_timestamp()}},
- )
- updated = await repo.find_ws_membership(ws_id, target_uid)
- return await _membership_to_out(updated, "ws_role")
-
-
-async def remove_org_member(uid: str, org_id: str, target_uid: str) -> None:
- caller = await repo.find_org_membership(org_id, uid)
- if not caller or caller["org_role"] not in ("owner", "admin"):
- raise HTTPException(403, "Org admin required")
- target = await repo.find_org_membership(org_id, target_uid)
- if not target:
- raise HTTPException(404, "Target not in org")
- if target["org_role"] == "owner":
- owners = [
- m for m in await repo.find_org_members(org_id)
- if m["org_role"] == "owner"
- ]
- if len(owners) == 1:
- raise HTTPException(400, "Cannot remove sole owner")
- # Delete org membership
- await db_manager.delete_one(
- ORG_MEMBERSHIPS, {"org_id": org_id, "uid": target_uid},
- )
- # Cascade — drop all ws memberships in this org for target
- ws_in_org = await db_manager.find(WORKSPACES, {"org_id": org_id}, limit=500)
- ws_ids = [w["_id"] for w in ws_in_org]
- if ws_ids:
- await db_manager.delete_many(
- WORKSPACE_MEMBERSHIPS,
- {"workspace_id": {"$in": ws_ids}, "uid": target_uid},
- )
-
-
-async def remove_workspace_member(uid: str, ws_id: str, target_uid: str) -> None:
- ws = await repo.find_workspace(ws_id)
- if not ws:
- raise HTTPException(404, "Workspace not found")
- caller_org_mem = await repo.find_org_membership(ws["org_id"], uid)
- caller_ws_mem = await repo.find_ws_membership(ws_id, uid)
- is_admin = (
- (caller_org_mem and caller_org_mem["org_role"] in ("owner", "admin"))
- or (caller_ws_mem and caller_ws_mem["ws_role"] == "admin")
- )
- if not is_admin:
- raise HTTPException(403, "Workspace admin required")
- await db_manager.delete_one(
- WORKSPACE_MEMBERSHIPS, {"workspace_id": ws_id, "uid": target_uid},
- )
- # H-1: the removed member's client already holds the unwrapped DEK, so deleting
- # their membership does not revoke decryption. Flag the workspace so an admin
- # rotates the shared key (which re-encrypts entries under a key the removed
- # member never receives).
- from app.api.routes.workspaces import crypto_repo
- await crypto_repo.flag_workspace_rotation_required(ws_id)
diff --git a/apps/backend/app/api/routes/workspaces/middleware.py b/apps/backend/app/api/routes/workspaces/middleware.py
index fcd83f17..55509097 100644
--- a/apps/backend/app/api/routes/workspaces/middleware.py
+++ b/apps/backend/app/api/routes/workspaces/middleware.py
@@ -2,12 +2,11 @@
from typing import Annotated, Any
-from fastapi import Depends, HTTPException, Request, status
+from fastapi import Depends, HTTPException, Request
from pydantic import BaseModel
from app.api.routes.auth.services import get_current_uid
from app.api.routes.workspaces.repo import (
- find_org_membership,
find_user_workspaces,
find_workspace,
find_ws_membership,
@@ -19,7 +18,6 @@
class WorkspaceContext(BaseModel):
uid: str
- org_id: str
workspace_id: str
ws_role: WsRole
is_personal: bool
@@ -41,30 +39,35 @@ async def get_workspace_ctx(
request: Request,
uid: Annotated[str, Depends(get_current_uid)],
) -> WorkspaceContext:
- ws_id = request.cookies.get(ACTIVE_WS_COOKIE)
- if not ws_id:
- ws_id = await default_personal_ws_id(uid)
+ cookie_ws_id = request.cookies.get(ACTIVE_WS_COOKIE)
+ ws_id = cookie_ws_id or await default_personal_ws_id(uid)
if not ws_id:
raise HTTPException(403, "No accessible workspace.")
ws = await find_workspace(ws_id)
+ mem = await find_ws_membership(ws_id, uid) if ws else None
+
+ # A stale/inaccessible `active_workspace` cookie (e.g. a workspace the user
+ # only reached via the old org-admin cascade, or one since removed) must not
+ # 403 every scoped request — that reads as "all my data vanished". Fall back
+ # to the user's personal workspace, which the frontend also resolves to.
+ if (not ws or not mem) and cookie_ws_id:
+ fallback_id = await default_personal_ws_id(uid)
+ if fallback_id and fallback_id != ws_id:
+ ws_id = fallback_id
+ ws = await find_workspace(ws_id)
+ mem = await find_ws_membership(ws_id, uid) if ws else None
+
if not ws:
raise HTTPException(403, "Workspace not found.")
-
- mem = await find_ws_membership(ws_id, uid)
- if mem:
- ws_role = mem["ws_role"]
- else:
- org_mem = await find_org_membership(ws["org_id"], uid)
- if not org_mem or org_mem["org_role"] not in ("owner", "admin"):
- raise HTTPException(403, "Not a member of this workspace.")
- ws_role = "admin" # implicit cascade
+ if not mem:
+ raise HTTPException(403, "Not a member of this workspace.")
+ ws_role = mem["ws_role"]
enc_settings = (ws.get("settings") or {}).get("encryption")
has_encryption = enc_settings is not None
return WorkspaceContext(
uid=uid,
- org_id=ws["org_id"],
workspace_id=ws_id,
ws_role=ws_role,
is_personal=bool(ws.get("is_personal")),
@@ -91,7 +94,7 @@ async def get_workspace_write_ctx(
def apply_workspace_filter(
ctx: WorkspaceContext, base_filter: dict[str, Any]
) -> dict[str, Any]:
- flt = {**base_filter, "org_id": ctx.org_id, "workspace_id": ctx.workspace_id}
+ flt = {**base_filter, "workspace_id": ctx.workspace_id}
if ctx.is_personal:
assert ctx.owner_uid is not None, (
f"Personal workspace {ctx.workspace_id} has no owner_uid — "
@@ -114,7 +117,6 @@ def apply_legacy_or_filter(
so it cannot leak across users.
"""
stamped: dict[str, Any] = {
- "org_id": ctx.org_id,
"workspace_id": ctx.workspace_id,
}
if ctx.is_personal:
diff --git a/apps/backend/app/api/routes/workspaces/repo.py b/apps/backend/app/api/routes/workspaces/repo.py
index a35f86e7..20946e9d 100644
--- a/apps/backend/app/api/routes/workspaces/repo.py
+++ b/apps/backend/app/api/routes/workspaces/repo.py
@@ -2,41 +2,15 @@
from pymongo.errors import DuplicateKeyError
from app.database import db_manager
from app.utils.collection_name import (
- ORGANIZATIONS,
- ORG_MEMBERSHIPS,
WORKSPACES,
WORKSPACE_MEMBERSHIPS,
)
from app.utils.utils import create_timestamp, new_id
-async def find_user_orgs(uid: str) -> list[dict[str, Any]]:
+async def find_user_workspaces(uid: str) -> list[dict[str, Any]]:
memberships = await db_manager.find(
- ORG_MEMBERSHIPS, {"uid": uid}, limit=100
- )
- if not memberships:
- return []
- org_ids = [m["org_id"] for m in memberships]
- orgs = await db_manager.find(
- ORGANIZATIONS, {"_id": {"$in": org_ids}, "deleted_at": None}, limit=100
- )
- by_id = {o["_id"]: o for o in orgs}
- out: list[dict[str, Any]] = []
- for m in memberships:
- org = by_id.get(m["org_id"])
- if org:
- out.append({**org, "org_role": m["org_role"]})
- return out
-
-
-async def find_user_workspaces(
- uid: str, org_id: str | None = None
-) -> list[dict[str, Any]]:
- flt: dict[str, Any] = {"uid": uid}
- if org_id is not None:
- flt["org_id"] = org_id
- memberships = await db_manager.find(
- WORKSPACE_MEMBERSHIPS, flt, limit=500
+ WORKSPACE_MEMBERSHIPS, {"uid": uid}, limit=500
)
if not memberships:
return []
@@ -57,16 +31,6 @@ async def find_workspace(workspace_id: str) -> dict[str, Any] | None:
return await db_manager.find_one(WORKSPACES, {"_id": workspace_id})
-async def find_org(org_id: str) -> dict[str, Any] | None:
- return await db_manager.find_one(ORGANIZATIONS, {"_id": org_id})
-
-
-async def find_org_membership(org_id: str, uid: str) -> dict[str, Any] | None:
- return await db_manager.find_one(
- ORG_MEMBERSHIPS, {"org_id": org_id, "uid": uid}
- )
-
-
async def find_ws_membership(
workspace_id: str, uid: str
) -> dict[str, Any] | None:
@@ -75,73 +39,16 @@ async def find_ws_membership(
)
-async def upsert_org(
- name: str, slug: str, kind: str, owner_uid: str | None
-) -> str:
- existing = await db_manager.find_one(ORGANIZATIONS, {"slug": slug})
- if existing:
- return existing["_id"]
- ts = create_timestamp()
- doc = {
- "_id": new_id(),
- "name": name,
- "slug": slug,
- "kind": kind,
- "owner_uid": owner_uid,
- "settings": {},
- "createdAt": ts,
- "updatedAt": ts,
- }
- try:
- await db_manager.insert_one(ORGANIZATIONS, doc)
- return doc["_id"]
- except DuplicateKeyError:
- # Concurrent caller won the race — re-read and return their id.
- existing = await db_manager.find_one(ORGANIZATIONS, {"slug": slug})
- if not existing:
- raise
- return existing["_id"]
-
-
-async def upsert_org_membership(
- org_id: str, uid: str, org_role: str
-) -> None:
- existing = await db_manager.find_one(
- ORG_MEMBERSHIPS, {"org_id": org_id, "uid": uid}
- )
- if existing:
- return
- try:
- await db_manager.insert_one(
- ORG_MEMBERSHIPS,
- {
- "_id": new_id(),
- "org_id": org_id,
- "uid": uid,
- "org_role": org_role,
- "createdAt": create_timestamp(),
- },
- )
- except DuplicateKeyError:
- # Concurrent caller won the race — membership already exists, nothing to do.
- existing = await db_manager.find_one(
- ORG_MEMBERSHIPS, {"org_id": org_id, "uid": uid}
- )
- if not existing:
- raise
-
-
def _personal_slug(uid: str) -> str:
return f"personal-{uid[:12]}"
# ponytail: workspace creation race tolerated — workspace_setup_at short-circuit in T3
# prevents repeat calls per user. Tighten if a workspace dup is ever observed in prod.
-async def upsert_personal_workspace(org_id: str, owner_uid: str) -> str:
+async def upsert_personal_workspace(owner_uid: str) -> str:
existing = await db_manager.find_one(
WORKSPACES,
{
- "org_id": org_id,
"owner_uid": owner_uid,
"is_personal": True,
},
@@ -151,7 +58,6 @@ async def upsert_personal_workspace(org_id: str, owner_uid: str) -> str:
ts = create_timestamp()
doc = {
"_id": new_id(),
- "org_id": org_id,
"name": "Personal",
"slug": _personal_slug(owner_uid),
"is_personal": True,
@@ -166,7 +72,7 @@ async def upsert_personal_workspace(org_id: str, owner_uid: str) -> str:
async def upsert_ws_membership(
- workspace_id: str, org_id: str, uid: str, ws_role: str
+ workspace_id: str, uid: str, ws_role: str
) -> None:
existing = await db_manager.find_one(
WORKSPACE_MEMBERSHIPS,
@@ -180,7 +86,6 @@ async def upsert_ws_membership(
{
"_id": new_id(),
"workspace_id": workspace_id,
- "org_id": org_id,
"uid": uid,
"ws_role": ws_role,
"createdAt": create_timestamp(),
@@ -195,25 +100,8 @@ async def upsert_ws_membership(
raise
-async def set_org_deleted(org_id: str, deleted_at: int | None) -> None:
- await db_manager.update_one(
- ORGANIZATIONS, {"_id": org_id},
- {"$set": {"deleted_at": deleted_at, "updatedAt": create_timestamp()}},
- )
-
-
async def set_workspace_deleted(workspace_id: str, deleted_at: int | None) -> None:
await db_manager.update_one(
WORKSPACES, {"_id": workspace_id},
{"$set": {"deleted_at": deleted_at, "updatedAt": create_timestamp()}},
)
-
-
-async def find_org_members(org_id: str) -> list[dict[str, Any]]:
- return await db_manager.find(ORG_MEMBERSHIPS, {"org_id": org_id}, limit=500)
-
-
-async def find_workspace_members(workspace_id: str) -> list[dict[str, Any]]:
- return await db_manager.find(
- WORKSPACE_MEMBERSHIPS, {"workspace_id": workspace_id}, limit=500,
- )
diff --git a/apps/backend/app/api/routes/workspaces/schema.py b/apps/backend/app/api/routes/workspaces/schema.py
index 754f269b..4728d97a 100644
--- a/apps/backend/app/api/routes/workspaces/schema.py
+++ b/apps/backend/app/api/routes/workspaces/schema.py
@@ -1,28 +1,17 @@
from typing import Literal
from pydantic import BaseModel, Field
-OrgKind = Literal["system", "user"]
-OrgRole = Literal["owner", "admin", "member", "viewer"]
WsKind = Literal["personal", "shared"]
WsRole = Literal["admin", "developer", "viewer"]
-class OrgOut(BaseModel):
- id: str
- name: str
- slug: str
- kind: OrgKind
- org_role: OrgRole
-
-
class WorkspaceEncryptionInfo(BaseModel):
scheme: str
dekFingerprint: str
createdAt: int
rotatedAt: int | None = None
- # Set true when a member is removed; cleared on the next DEK rotation.
- # Signals admins that the shared key must be rotated to revoke the removed
- # member's decryption capability.
+ # Retained for schema compatibility with existing encrypted workspaces.
+ # No longer set now that member sharing is removed (single-user workspaces).
rotationRequired: bool = False
@@ -32,7 +21,6 @@ class WorkspaceSettings(BaseModel):
class WorkspaceOut(BaseModel):
id: str
- org_id: str
name: str
slug: str
is_personal: bool
@@ -49,61 +37,6 @@ class SetActiveWorkspaceResponse(BaseModel):
workspace_id: str
-InvitationStatus = Literal["pending", "accepted", "revoked", "expired", "wrapping_pending"]
-
-
-class OrgCreate(BaseModel):
- name: str = Field(min_length=1, max_length=80)
- slug: str | None = Field(default=None, min_length=1, max_length=80)
-
-
-class OrgPatch(BaseModel):
- name: str | None = Field(default=None, min_length=1, max_length=80)
-
-
-class WorkspaceCreate(BaseModel):
- name: str = Field(min_length=1, max_length=80)
- slug: str | None = Field(default=None, min_length=1, max_length=80)
-
-
-class WorkspacePatch(BaseModel):
- name: str | None = Field(default=None, min_length=1, max_length=80)
-
-
-class MemberOut(BaseModel):
- uid: str
- email: str | None
- display_name: str | None = None
- role: str
- since: int
-
-
-class InviteMemberRequest(BaseModel):
- email: str = Field(min_length=1)
- role: str
- # Org-level invite only — optionally also grant a workspace membership on accept.
- workspace_id: str | None = None
- workspace_role: str | None = None
-
-
-class ChangeRoleRequest(BaseModel):
- role: str
-
-
-class InvitationOut(BaseModel):
- id: str
- org_id: str
- workspace_id: str | None
- invited_email: str
- invited_uid: str | None
- invited_role_org: OrgRole | None
- invited_role_ws: WsRole | None
- status: InvitationStatus
- token: str
- expires_at: int
- created_at: int
-
-
class EncryptionBlob(BaseModel):
encrypted: str
iv: str
@@ -120,38 +53,3 @@ class KeypairPostRequest(BaseModel):
publicKey: str = Field(min_length=1)
privateKeyEncrypted: EncryptionBlob
salt: str = Field(min_length=1)
-
-
-class WrappedDekBlob(BaseModel):
- encrypted: str
- iv: str
- senderPublicKey: str
-
-
-class DekWrapOut(BaseModel):
- wrappedDek: WrappedDekBlob | None
- wrappedDekVersion: int
- # SHA-256 fingerprint of the current workspace DEK (from workspace settings).
- # Client verifies the unwrapped DEK matches this before trusting it (M-1).
- expectedFingerprint: str | None = None
-
-
-class DekWrapPostRequest(BaseModel):
- target_uid: str
- wrapped: WrappedDekBlob
-
-
-class WrapForMember(BaseModel):
- uid: str
- wrapped: WrappedDekBlob
-
-
-class RotateDekRequest(BaseModel):
- dekFingerprint: str = Field(min_length=1)
- wraps: list[WrapForMember]
-
-
-class PendingWrapOut(BaseModel):
- uid: str
- email: str | None
- publicKey: str | None
diff --git a/apps/backend/app/api/routes/workspaces/seed.py b/apps/backend/app/api/routes/workspaces/seed.py
deleted file mode 100644
index ba0f6d3a..00000000
--- a/apps/backend/app/api/routes/workspaces/seed.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from app.api.routes.workspaces.repo import upsert_org
-from app.database import db_manager
-from app.utils.collection_name import ORGANIZATIONS
-
-SYSTEM_ORG_NAME = "MyDevTools Cloud"
-SYSTEM_ORG_SLUG = "mydevtools-cloud"
-
-
-async def ensure_system_org() -> str:
- """Idempotently create the system org. Returns its id."""
- return await upsert_org(
- name=SYSTEM_ORG_NAME,
- slug=SYSTEM_ORG_SLUG,
- kind="system",
- owner_uid=None,
- )
-
-
-async def get_system_org_id() -> str | None:
- """Return the singleton system org id, or None if not yet seeded."""
- doc = await db_manager.find_one(ORGANIZATIONS, {"slug": SYSTEM_ORG_SLUG})
- if not doc:
- return None
- return doc["_id"]
diff --git a/apps/backend/app/api/routes/workspaces/services.py b/apps/backend/app/api/routes/workspaces/services.py
index c5495b17..3b88ab44 100644
--- a/apps/backend/app/api/routes/workspaces/services.py
+++ b/apps/backend/app/api/routes/workspaces/services.py
@@ -2,11 +2,9 @@
from app.api.routes.auth import users_repo
from app.api.routes.workspaces.repo import (
- upsert_org_membership,
upsert_personal_workspace,
upsert_ws_membership,
)
-from app.api.routes.workspaces.seed import ensure_system_org
from app.api.routes.workspaces.backfill import schedule_backfill
@@ -14,8 +12,8 @@ async def ensure_user_workspace_setup(
uid: str,
background_tasks: BackgroundTasks | None = None,
) -> str:
- """Idempotently create the user's MyDevTools Cloud membership and
- Personal workspace. Returns the user's Personal workspace_id.
+ """Idempotently create the user's Personal workspace.
+ Returns the user's Personal workspace_id.
"""
setup_at = await users_repo.get_workspace_setup_at(uid)
if setup_at:
@@ -23,10 +21,8 @@ async def ensure_user_workspace_setup(
if ws_id:
return ws_id
- org_id = await ensure_system_org()
- await upsert_org_membership(org_id, uid, "member")
- ws_id = await upsert_personal_workspace(org_id, uid)
- await upsert_ws_membership(ws_id, org_id, uid, "admin")
+ ws_id = await upsert_personal_workspace(uid)
+ await upsert_ws_membership(ws_id, uid, "admin")
await users_repo.mark_workspace_setup(uid, ws_id)
@@ -34,6 +30,6 @@ async def ensure_user_workspace_setup(
if not migrated_at:
await users_repo.mark_migration_pending(uid)
if background_tasks is not None:
- schedule_backfill(background_tasks, uid, ws_id, org_id)
+ schedule_backfill(background_tasks, uid, ws_id)
return ws_id
diff --git a/apps/backend/app/api/routes/workspaces/sweeper.py b/apps/backend/app/api/routes/workspaces/sweeper.py
deleted file mode 100644
index d5a26e33..00000000
--- a/apps/backend/app/api/routes/workspaces/sweeper.py
+++ /dev/null
@@ -1,71 +0,0 @@
-import asyncio
-import logging
-from app.database import db_manager
-from app.utils.collection_name import (
- INVITATIONS, ORGANIZATIONS, WORKSPACES,
- ORG_MEMBERSHIPS, WORKSPACE_MEMBERSHIPS,
-)
-from app.utils.utils import create_timestamp
-
-log = logging.getLogger(__name__)
-SOFT_DELETE_RETENTION_MS = 30 * 24 * 3600 * 1000
-
-
-async def _hard_delete_orgs(threshold: int) -> int:
- olds = await db_manager.find(
- ORGANIZATIONS,
- {"deleted_at": {"$ne": None, "$lt": threshold}},
- limit=500,
- )
- if not olds:
- return 0
- org_ids = [o["_id"] for o in olds]
- await db_manager.delete_many(ORG_MEMBERSHIPS, {"org_id": {"$in": org_ids}})
- await db_manager.delete_many(WORKSPACE_MEMBERSHIPS, {"org_id": {"$in": org_ids}})
- await db_manager.delete_many(WORKSPACES, {"org_id": {"$in": org_ids}})
- await db_manager.delete_many(ORGANIZATIONS, {"_id": {"$in": org_ids}})
- return len(org_ids)
-
-
-async def _hard_delete_workspaces(threshold: int) -> int:
- olds = await db_manager.find(
- WORKSPACES,
- {"deleted_at": {"$ne": None, "$lt": threshold}, "is_personal": False},
- limit=500,
- )
- if not olds:
- return 0
- ws_ids = [w["_id"] for w in olds]
- await db_manager.delete_many(WORKSPACE_MEMBERSHIPS, {"workspace_id": {"$in": ws_ids}})
- await db_manager.delete_many(WORKSPACES, {"_id": {"$in": ws_ids}})
- return len(ws_ids)
-
-
-async def _expire_invitations(now: int) -> int:
- res = await db_manager.update_many(
- INVITATIONS,
- {"status": "pending", "expires_at": {"$lt": now}},
- {"$set": {"status": "expired"}},
- )
- return getattr(res, "modified_count", 0)
-
-
-async def run_sweeper_once() -> dict[str, int]:
- now = create_timestamp()
- threshold = now - SOFT_DELETE_RETENTION_MS
- return {
- "orgs_hard_deleted": await _hard_delete_orgs(threshold),
- "workspaces_hard_deleted": await _hard_delete_workspaces(threshold),
- "invitations_expired": await _expire_invitations(now),
- }
-
-
-async def sweeper_loop(interval_seconds: int = 3600) -> None:
- while True:
- try:
- stats = await run_sweeper_once()
- if any(stats.values()):
- log.info("sweeper: %s", stats)
- except Exception as exc:
- log.warning("sweeper error: %s", exc)
- await asyncio.sleep(interval_seconds)
diff --git a/apps/backend/app/core/email.py b/apps/backend/app/core/email.py
deleted file mode 100644
index d46b8f9f..00000000
--- a/apps/backend/app/core/email.py
+++ /dev/null
@@ -1,63 +0,0 @@
-import asyncio
-import logging
-from urllib.parse import urlencode
-
-from app.core.config import get_settings
-
-log = logging.getLogger(__name__)
-
-
-def _build_invite_url(token: str) -> str:
- settings = get_settings()
- qs = urlencode({"invite": token})
- return f"{settings.APP_PUBLIC_URL.rstrip('/')}/login?{qs}"
-
-
-def _render_html(*, inviter_name: str, org_name: str, workspace_name: str | None, invite_url: str) -> str:
- target = f"{org_name} / {workspace_name}" if workspace_name else org_name
- return f"""
-
Hi,
- {inviter_name} invited you to join {target} on MyDevTools.
- Accept invitation
- This link is valid for 14 days.
- """
-
-
-async def send_invitation_email(
- *,
- to: str,
- token: str,
- inviter_name: str,
- org_name: str,
- workspace_name: str | None,
-) -> None:
- invite_url = _build_invite_url(token)
- settings = get_settings()
- if not settings.RESEND_API_KEY:
- log.info(
- "DEV email: would send to=%s token=%s url=%s org=%s workspace=%s",
- to, token, invite_url, org_name, workspace_name,
- )
- return
-
- import resend
-
- resend.api_key = settings.RESEND_API_KEY
- html = _render_html(
- inviter_name=inviter_name,
- org_name=org_name,
- workspace_name=workspace_name,
- invite_url=invite_url,
- )
- target = f"{org_name}" + (f" / {workspace_name}" if workspace_name else "")
-
- # resend.Emails.send is sync, so wrap it with asyncio.to_thread
- await asyncio.to_thread(
- resend.Emails.send,
- {
- "from": settings.INVITATION_FROM_EMAIL,
- "to": [to],
- "subject": f"You're invited to {target} on MyDevTools",
- "html": html,
- },
- )
diff --git a/apps/backend/app/core/indexes.py b/apps/backend/app/core/indexes.py
index 1917a156..a42a711e 100644
--- a/apps/backend/app/core/indexes.py
+++ b/apps/backend/app/core/indexes.py
@@ -25,11 +25,8 @@
USER_PREFERENCES,
USERS,
WEBAUTHN_CHALLENGES,
- ORGANIZATIONS,
- ORG_MEMBERSHIPS,
WORKSPACES,
WORKSPACE_MEMBERSHIPS,
- INVITATIONS,
)
from app.core.config import get_settings
@@ -77,14 +74,9 @@ async def ensure_indexes() -> None:
await db_manager.create_index(AUDIT_LOG, [("uid", 1), ("ts", -1)])
await db_manager.create_index(AUDIT_LOG, [("uid", 1), ("module", 1), ("ts", -1)])
await db_manager.create_index(AUDIT_LOG, "expireAt", expire_after_seconds=0)
- await db_manager.create_index(ORGANIZATIONS, [("slug", 1)], unique=True)
- await db_manager.create_index(
- ORG_MEMBERSHIPS, [("org_id", 1), ("uid", 1)], unique=True
- )
- await db_manager.create_index(ORG_MEMBERSHIPS, [("uid", 1)])
await db_manager.create_index(
WORKSPACES,
- [("org_id", 1), ("owner_uid", 1), ("is_personal", 1)],
+ [("owner_uid", 1), ("is_personal", 1)],
)
await db_manager.create_index(
WORKSPACE_MEMBERSHIPS,
@@ -92,7 +84,3 @@ async def ensure_indexes() -> None:
unique=True,
)
await db_manager.create_index(WORKSPACE_MEMBERSHIPS, [("uid", 1)])
- await db_manager.create_index(INVITATIONS, [("token", 1)], unique=True)
- await db_manager.create_index(INVITATIONS, [("invited_email", 1), ("status", 1)])
- await db_manager.create_index(INVITATIONS, [("org_id", 1), ("status", 1)])
- await db_manager.create_index(INVITATIONS, [("expires_at", 1), ("status", 1)])
diff --git a/apps/backend/app/main.py b/apps/backend/app/main.py
index 7703281b..9c24cee5 100644
--- a/apps/backend/app/main.py
+++ b/apps/backend/app/main.py
@@ -25,21 +25,12 @@ async def lifespan(_app: FastAPI):
except Exception as exc:
logging.getLogger(__name__).warning("Index creation failed: %s", exc)
- try:
- from app.api.routes.workspaces.seed import ensure_system_org
- await ensure_system_org()
- except Exception as exc:
- logging.getLogger(__name__).warning("System org seed failed: %s", exc)
-
from app.core.redis_client import open_redis, close_redis
await open_redis()
from app.api.routes.url_shortener.click_queue import flush_loop, close_flush_client
click_flush_task = asyncio.create_task(flush_loop())
- from app.api.routes.workspaces.sweeper import sweeper_loop
- sweeper_task = asyncio.create_task(sweeper_loop())
-
try:
yield
finally:
@@ -48,11 +39,6 @@ async def lifespan(_app: FastAPI):
await click_flush_task
except asyncio.CancelledError:
pass
- sweeper_task.cancel()
- try:
- await sweeper_task
- except asyncio.CancelledError:
- pass
await close_flush_client()
await close_redis()
diff --git a/apps/backend/app/utils/collection_name.py b/apps/backend/app/utils/collection_name.py
index ccd2e649..64a6ee9a 100644
--- a/apps/backend/app/utils/collection_name.py
+++ b/apps/backend/app/utils/collection_name.py
@@ -27,9 +27,6 @@
URL_CLICK_EVENTS = "url_click_events"
AUDIT_LOG = "audit_log"
WEBAUTHN_CHALLENGES = "webauthn_challenges"
-ORGANIZATIONS = "organizations"
-ORG_MEMBERSHIPS = "org_memberships"
WORKSPACES = "workspaces"
WORKSPACE_MEMBERSHIPS = "workspace_memberships"
-INVITATIONS = "invitations"
COUNTERS = "counters"
diff --git a/apps/backend/tests/api/routes/api_client/test_collections_delta.py b/apps/backend/tests/api/routes/api_client/test_collections_delta.py
index 2b042288..513eb3f8 100644
--- a/apps/backend/tests/api/routes/api_client/test_collections_delta.py
+++ b/apps/backend/tests/api/routes/api_client/test_collections_delta.py
@@ -28,7 +28,6 @@
BASE_COLLECTION: dict[str, Any] = {
"_id": COLLECTION_OID,
"created_by": OWNER_UID,
- "org_id": "org-test",
"workspace_id": "ws-owner",
"owner_uid": OWNER_UID,
"name": "My Collection",
@@ -56,10 +55,9 @@
}
-def _make_ctx(uid: str, ws_id: str = "ws-owner", org_id: str = "org-test") -> WorkspaceContext:
+def _make_ctx(uid: str, ws_id: str = "ws-owner") -> WorkspaceContext:
return WorkspaceContext(
uid=uid,
- org_id=org_id,
workspace_id=ws_id,
ws_role="admin",
is_personal=True,
diff --git a/apps/backend/tests/api/routes/webhook_tester/__init__.py b/apps/backend/tests/api/routes/webhook_tester/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/backend/tests/api/routes/webhook_tester/test_webhook_tester.py b/apps/backend/tests/api/routes/webhook_tester/test_webhook_tester.py
new file mode 100644
index 00000000..4437d351
--- /dev/null
+++ b/apps/backend/tests/api/routes/webhook_tester/test_webhook_tester.py
@@ -0,0 +1,200 @@
+"""Webhook Tester route tests.
+
+The router is not yet wired into app.api.router (the coordinator adds the
+include line), so we mount it on the app here. REDIS_URL is unset in tests,
+so the service's in-memory fallback store is exercised.
+"""
+from datetime import datetime, timedelta, timezone
+
+import pytest
+from fastapi.testclient import TestClient
+
+from app.api.routes.webhook_tester import services as svc
+from app.api.routes.webhook_tester.api import router as webhook_tester_router
+from app.main import app
+
+if not any(getattr(r, "path", "").startswith("/api/v1/webhook-tester") for r in app.routes):
+ app.include_router(webhook_tester_router, prefix="/api/v1")
+
+client = TestClient(app)
+
+BASE = "/api/v1/webhook-tester"
+
+
+@pytest.fixture(autouse=True)
+def clean_store():
+ svc._memory_bins.clear()
+ yield
+ svc._memory_bins.clear()
+
+
+def _create_bin() -> dict:
+ res = client.post(f"{BASE}/bins")
+ assert res.status_code == 200
+ return res.json()
+
+
+def test_create_bin_shape_and_ttl() -> None:
+ data = _create_bin()
+ assert set(data) == {"bin_id", "created_at", "expires_at"}
+ assert 8 <= len(data["bin_id"]) <= 16
+ created = datetime.fromisoformat(data["created_at"])
+ expires = datetime.fromisoformat(data["expires_at"])
+ assert expires - created == timedelta(hours=24)
+
+
+def test_bin_ids_are_unique() -> None:
+ ids = {_create_bin()["bin_id"] for _ in range(5)}
+ assert len(ids) == 5
+
+
+def test_receive_records_request_details() -> None:
+ bin_id = _create_bin()["bin_id"]
+ res = client.post(
+ f"{BASE}/in/{bin_id}?foo=bar&foo=baz&x=1",
+ json={"hello": "world"},
+ headers={"x-custom": "abc"},
+ )
+ assert res.status_code == 200
+ assert res.json() == {"ok": True}
+
+ listed = client.get(f"{BASE}/bins/{bin_id}/requests")
+ assert listed.status_code == 200
+ body = listed.json()
+ assert body["bin_id"] == bin_id
+ assert body["count"] == 1
+ req = body["requests"][0]
+ assert req["method"] == "POST"
+ assert req["path"] == ""
+ assert req["query"] == {"foo": "bar,baz", "x": "1"}
+ assert req["headers"]["x-custom"] == "abc"
+ assert req["body"] == '{"hello":"world"}'
+ assert req["body_encoding"] == "text"
+ assert req["body_truncated"] is False
+ assert req["content_type"] == "application/json"
+ assert req["size"] == len('{"hello":"world"}')
+ assert req["id"]
+ assert req["received_at"]
+
+
+def test_receive_captures_path_suffix() -> None:
+ bin_id = _create_bin()["bin_id"]
+ res = client.put(f"{BASE}/in/{bin_id}/github/push/events", content=b"payload")
+ assert res.status_code == 200
+ req = client.get(f"{BASE}/bins/{bin_id}/requests").json()["requests"][0]
+ assert req["method"] == "PUT"
+ assert req["path"] == "github/push/events"
+ assert req["body"] == "payload"
+
+
+@pytest.mark.parametrize("method", ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"])
+def test_receive_accepts_all_methods(method: str) -> None:
+ bin_id = _create_bin()["bin_id"]
+ res = client.request(method, f"{BASE}/in/{bin_id}")
+ assert res.status_code == 200
+ reqs = client.get(f"{BASE}/bins/{bin_id}/requests").json()["requests"]
+ assert [r["method"] for r in reqs] == [method]
+
+
+def test_receive_unknown_bin_404() -> None:
+ assert client.post(f"{BASE}/in/does-not-exist").status_code == 404
+
+
+def test_list_unknown_bin_404() -> None:
+ assert client.get(f"{BASE}/bins/does-not-exist/requests").status_code == 404
+
+
+def test_clear_unknown_bin_404() -> None:
+ assert client.delete(f"{BASE}/bins/does-not-exist/requests").status_code == 404
+
+
+def test_expired_bin_rejected() -> None:
+ bin_id = _create_bin()["bin_id"]
+ past = (datetime.now(timezone.utc) - timedelta(seconds=1)).isoformat()
+ svc._memory_bins[bin_id]["expires_at"] = past
+ assert client.post(f"{BASE}/in/{bin_id}").status_code == 404
+ assert client.get(f"{BASE}/bins/{bin_id}/requests").status_code == 404
+
+
+def test_newest_first_and_capped_at_100() -> None:
+ bin_id = _create_bin()["bin_id"]
+ for i in range(105):
+ client.post(f"{BASE}/in/{bin_id}", content=f"n{i}".encode())
+ body = client.get(f"{BASE}/bins/{bin_id}/requests").json()
+ assert body["count"] == 100
+ assert body["requests"][0]["body"] == "n104"
+ assert body["requests"][-1]["body"] == "n5" # oldest 5 dropped
+
+
+def test_after_cursor_by_request_id() -> None:
+ bin_id = _create_bin()["bin_id"]
+ for i in range(3):
+ client.post(f"{BASE}/in/{bin_id}", content=f"n{i}".encode())
+ all_reqs = client.get(f"{BASE}/bins/{bin_id}/requests").json()["requests"]
+ cursor = all_reqs[1]["id"] # the middle (n1)
+ newer = client.get(f"{BASE}/bins/{bin_id}/requests", params={"after": cursor}).json()
+ assert newer["count"] == 1
+ assert newer["requests"][0]["body"] == "n2"
+
+
+def test_after_cursor_by_timestamp() -> None:
+ bin_id = _create_bin()["bin_id"]
+ client.post(f"{BASE}/in/{bin_id}", content=b"old")
+ old = client.get(f"{BASE}/bins/{bin_id}/requests").json()["requests"][0]
+ client.post(f"{BASE}/in/{bin_id}", content=b"new")
+ newer = client.get(
+ f"{BASE}/bins/{bin_id}/requests", params={"after": old["received_at"]}
+ ).json()["requests"]
+ assert [r["body"] for r in newer] == ["new"]
+
+
+def test_after_unknown_cursor_returns_everything() -> None:
+ bin_id = _create_bin()["bin_id"]
+ client.post(f"{BASE}/in/{bin_id}", content=b"a")
+ body = client.get(f"{BASE}/bins/{bin_id}/requests", params={"after": "bogus"}).json()
+ assert body["count"] == 1
+
+
+def test_binary_body_is_base64() -> None:
+ bin_id = _create_bin()["bin_id"]
+ client.post(f"{BASE}/in/{bin_id}", content=b"\xff\xfe\x00binary")
+ req = client.get(f"{BASE}/bins/{bin_id}/requests").json()["requests"][0]
+ assert req["body_encoding"] == "base64"
+ import base64
+
+ assert base64.b64decode(req["body"]) == b"\xff\xfe\x00binary"
+
+
+def test_large_body_truncated_at_64kb() -> None:
+ bin_id = _create_bin()["bin_id"]
+ client.post(f"{BASE}/in/{bin_id}", content=b"a" * (svc.MAX_BODY_BYTES + 500))
+ req = client.get(f"{BASE}/bins/{bin_id}/requests").json()["requests"][0]
+ assert req["body_truncated"] is True
+ assert len(req["body"]) == svc.MAX_BODY_BYTES
+ assert req["size"] == svc.MAX_BODY_BYTES + 500
+
+
+def test_clear_requests() -> None:
+ bin_id = _create_bin()["bin_id"]
+ client.post(f"{BASE}/in/{bin_id}", content=b"x")
+ res = client.delete(f"{BASE}/bins/{bin_id}/requests")
+ assert res.status_code == 204
+ body = client.get(f"{BASE}/bins/{bin_id}/requests").json()
+ assert body["count"] == 0
+ # Bin itself still alive after clearing
+ assert client.post(f"{BASE}/in/{bin_id}", content=b"y").status_code == 200
+
+
+def test_source_ip_prefers_x_forwarded_for() -> None:
+ bin_id = _create_bin()["bin_id"]
+ client.post(f"{BASE}/in/{bin_id}", headers={"x-forwarded-for": "203.0.113.7, 10.0.0.1"})
+ req = client.get(f"{BASE}/bins/{bin_id}/requests").json()["requests"][0]
+ assert req["source_ip"] == "203.0.113.7"
+
+
+def test_endpoints_do_not_require_auth() -> None:
+ # No cookies / Authorization header anywhere in this file — reaching 200s
+ # above already proves it, but assert explicitly for the receive endpoint.
+ bin_id = _create_bin()["bin_id"]
+ res = client.post(f"{BASE}/in/{bin_id}", json={"ping": 1})
+ assert res.status_code == 200
diff --git a/apps/backend/tests/api/routes/whois/__init__.py b/apps/backend/tests/api/routes/whois/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/backend/tests/api/routes/whois/test_whois_lookup.py b/apps/backend/tests/api/routes/whois/test_whois_lookup.py
new file mode 100644
index 00000000..c8c695d7
--- /dev/null
+++ b/apps/backend/tests/api/routes/whois/test_whois_lookup.py
@@ -0,0 +1,294 @@
+import asyncio
+
+import httpx
+import pytest
+from fastapi import HTTPException
+
+from app.api.routes.whois import services as svc
+
+RDAP_DOMAIN = {
+ "objectClassName": "domain",
+ "ldhName": "EXAMPLE.COM",
+ "status": ["client delete prohibited", "client transfer prohibited"],
+ "events": [
+ {"eventAction": "registration", "eventDate": "1995-08-14T04:00:00Z"},
+ {"eventAction": "expiration", "eventDate": "2026-08-13T04:00:00Z"},
+ {"eventAction": "last changed", "eventDate": "2025-08-14T07:01:44Z"},
+ {"eventAction": "last update of RDAP database", "eventDate": "2026-01-01T00:00:00Z"},
+ ],
+ "entities": [
+ {
+ "objectClassName": "entity",
+ "handle": "376",
+ "roles": ["registrar"],
+ "vcardArray": [
+ "vcard",
+ [
+ ["version", {}, "text", "4.0"],
+ ["fn", {}, "text", "RESERVED-Internet Assigned Numbers Authority"],
+ ],
+ ],
+ "entities": [
+ {
+ "objectClassName": "entity",
+ "handle": "ABUSE-1",
+ "roles": ["abuse"],
+ "vcardArray": ["vcard", [["version", {}, "text", "4.0"], ["fn", {}, "text", "Abuse Desk"]]],
+ }
+ ],
+ },
+ {
+ "objectClassName": "entity",
+ "roles": ["registrant"],
+ "vcardArray": [
+ "vcard",
+ [
+ ["version", {}, "text", "4.0"],
+ ["fn", {}, "text", "REDACTED FOR PRIVACY"],
+ ["org", {}, "text", "Example Org"],
+ ],
+ ],
+ },
+ ],
+ "nameservers": [
+ {"ldhName": "A.IANA-SERVERS.NET."},
+ {"ldhName": "B.IANA-SERVERS.NET"},
+ {"ldhName": "a.iana-servers.net"},
+ ],
+}
+
+RDAP_IP = {
+ "objectClassName": "ip network",
+ "handle": "NET-8-8-8-0-1",
+ "name": "LVLT-GOGL-8-8-8",
+ "startAddress": "8.8.8.0",
+ "endAddress": "8.8.8.255",
+ "status": ["active"],
+ "events": [
+ {"eventAction": "registration", "eventDate": "2014-03-14T00:00:00Z"},
+ {"eventAction": "last changed", "eventDate": "2014-03-14T00:00:00Z"},
+ ],
+ "entities": [
+ {
+ "objectClassName": "entity",
+ "handle": "GOGL",
+ "roles": ["registrant"],
+ "vcardArray": ["vcard", [["version", {}, "text", "4.0"], ["fn", {}, "text", "Google LLC"]]],
+ }
+ ],
+}
+
+
+# ---------------------------------------------------------------------------
+# sanitize_target
+# ---------------------------------------------------------------------------
+
+def test_sanitize_target_plain_domain():
+ assert svc.sanitize_target("example.com") == ("example.com", "domain")
+
+
+def test_sanitize_target_strips_scheme_path_and_case():
+ assert svc.sanitize_target("https://Example.COM/path?q=1#frag") == ("example.com", "domain")
+
+
+def test_sanitize_target_strips_trailing_dot_and_port():
+ assert svc.sanitize_target("example.com.") == ("example.com", "domain")
+ assert svc.sanitize_target("example.com:8080") == ("example.com", "domain")
+
+
+def test_sanitize_target_ipv4():
+ assert svc.sanitize_target("8.8.8.8") == ("8.8.8.8", "ip")
+
+
+def test_sanitize_target_ipv6():
+ assert svc.sanitize_target("2001:4860:4860::8888") == ("2001:4860:4860::8888", "ip")
+ assert svc.sanitize_target("[2001:4860:4860::8888]") == ("2001:4860:4860::8888", "ip")
+
+
+@pytest.mark.parametrize("bad", ["", " ", "not a domain", "no-tld", "-bad-.com", "a" * 300 + ".com"])
+def test_sanitize_target_rejects_invalid(bad):
+ with pytest.raises(HTTPException) as exc:
+ svc.sanitize_target(bad)
+ assert exc.value.status_code == 400
+
+
+# ---------------------------------------------------------------------------
+# lookup_whois normalization (fetch mocked)
+# ---------------------------------------------------------------------------
+
+def test_lookup_whois_normalizes_domain(monkeypatch):
+ async def fake_fetch(*, kind, target):
+ assert kind == "domain"
+ assert target == "example.com"
+ return RDAP_DOMAIN
+
+ monkeypatch.setattr(svc, "fetch_rdap", fake_fetch)
+ out = asyncio.run(svc.lookup_whois("https://Example.com/whatever"))
+
+ assert out.target == "example.com"
+ assert out.type == "domain"
+ assert out.registrar == "RESERVED-Internet Assigned Numbers Authority"
+ assert out.status == ["client delete prohibited", "client transfer prohibited"]
+ assert out.created == "1995-08-14T04:00:00Z"
+ assert out.updated == "2025-08-14T07:01:44Z"
+ assert out.expires == "2026-08-13T04:00:00Z"
+ # lowercased, trailing dot stripped, deduped
+ assert out.nameservers == ["a.iana-servers.net", "b.iana-servers.net"]
+ assert out.raw == RDAP_DOMAIN
+
+
+def test_lookup_whois_contacts_include_nested_and_redacted(monkeypatch):
+ async def fake_fetch(*, kind, target):
+ return RDAP_DOMAIN
+
+ monkeypatch.setattr(svc, "fetch_rdap", fake_fetch)
+ out = asyncio.run(svc.lookup_whois("example.com"))
+
+ by_role = {tuple(c.roles): c for c in out.contacts}
+ registrar = by_role[("registrar",)]
+ assert registrar.name == "RESERVED-Internet Assigned Numbers Authority"
+ assert registrar.handle == "376"
+
+ registrant = by_role[("registrant",)]
+ assert registrant.name == "REDACTED FOR PRIVACY" # kept as the registry exposed it
+ assert registrant.organization == "Example Org"
+
+ abuse = by_role[("abuse",)] # nested inside the registrar entity
+ assert abuse.name == "Abuse Desk"
+ assert abuse.handle == "ABUSE-1"
+
+
+def test_lookup_whois_ip(monkeypatch):
+ async def fake_fetch(*, kind, target):
+ assert kind == "ip"
+ assert target == "8.8.8.8"
+ return RDAP_IP
+
+ monkeypatch.setattr(svc, "fetch_rdap", fake_fetch)
+ out = asyncio.run(svc.lookup_whois("8.8.8.8"))
+
+ assert out.type == "ip"
+ assert out.registrar == "LVLT-GOGL-8-8-8" # falls back to network name
+ assert out.created == "2014-03-14T00:00:00Z"
+ assert out.nameservers == []
+ assert any(c.name == "Google LLC" for c in out.contacts)
+
+
+def test_lookup_whois_unknown_domain_returns_404(monkeypatch):
+ async def fake_fetch(*, kind, target):
+ return None
+
+ monkeypatch.setattr(svc, "fetch_rdap", fake_fetch)
+ with pytest.raises(HTTPException) as exc:
+ asyncio.run(svc.lookup_whois("definitely-not-registered-zzz.com"))
+ assert exc.value.status_code == 404
+
+
+def test_lookup_whois_invalid_target_short_circuits(monkeypatch):
+ called = False
+
+ async def fake_fetch(*, kind, target):
+ nonlocal called
+ called = True
+ return RDAP_DOMAIN
+
+ monkeypatch.setattr(svc, "fetch_rdap", fake_fetch)
+ with pytest.raises(HTTPException) as exc:
+ asyncio.run(svc.lookup_whois("!!not valid!!"))
+ assert exc.value.status_code == 400
+ assert called is False
+
+
+# ---------------------------------------------------------------------------
+# fetch_rdap (httpx mocked)
+# ---------------------------------------------------------------------------
+
+class _FakeResponse:
+ def __init__(self, status_code=200, json_data=None):
+ self.status_code = status_code
+ self._json = json_data
+
+ def json(self):
+ if self._json is None:
+ raise ValueError("no json")
+ return self._json
+
+
+class _FakeAsyncClient:
+ init_kwargs = None
+ last_url = None
+ response = None
+ error = None
+
+ def __init__(self, **kwargs):
+ _FakeAsyncClient.init_kwargs = kwargs
+
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, *exc):
+ return False
+
+ async def get(self, url, **kwargs):
+ _FakeAsyncClient.last_url = url
+ if _FakeAsyncClient.error is not None:
+ raise _FakeAsyncClient.error
+ return _FakeAsyncClient.response
+
+
+@pytest.fixture
+def fake_http(monkeypatch):
+ _FakeAsyncClient.init_kwargs = None
+ _FakeAsyncClient.last_url = None
+ _FakeAsyncClient.response = None
+ _FakeAsyncClient.error = None
+ monkeypatch.setattr(svc.httpx, "AsyncClient", _FakeAsyncClient)
+ return _FakeAsyncClient
+
+
+def test_fetch_rdap_domain_url_and_redirects(fake_http):
+ fake_http.response = _FakeResponse(200, RDAP_DOMAIN)
+ out = asyncio.run(svc.fetch_rdap(kind="domain", target="example.com"))
+ assert out == RDAP_DOMAIN
+ assert fake_http.last_url == "https://rdap.org/domain/example.com"
+ assert fake_http.init_kwargs.get("follow_redirects") is True
+
+
+def test_fetch_rdap_ip_url(fake_http):
+ fake_http.response = _FakeResponse(200, RDAP_IP)
+ out = asyncio.run(svc.fetch_rdap(kind="ip", target="8.8.8.8"))
+ assert out == RDAP_IP
+ assert fake_http.last_url == "https://rdap.org/ip/8.8.8.8"
+
+
+def test_fetch_rdap_404_returns_none(fake_http):
+ fake_http.response = _FakeResponse(404)
+ assert asyncio.run(svc.fetch_rdap(kind="domain", target="nope.example")) is None
+
+
+def test_fetch_rdap_upstream_error_maps_to_502(fake_http):
+ fake_http.response = _FakeResponse(500)
+ with pytest.raises(HTTPException) as exc:
+ asyncio.run(svc.fetch_rdap(kind="domain", target="example.com"))
+ assert exc.value.status_code == 502
+
+
+def test_fetch_rdap_rate_limit_maps_to_503(fake_http):
+ fake_http.response = _FakeResponse(429)
+ with pytest.raises(HTTPException) as exc:
+ asyncio.run(svc.fetch_rdap(kind="domain", target="example.com"))
+ assert exc.value.status_code == 503
+
+
+def test_fetch_rdap_network_error_maps_to_502(fake_http):
+ fake_http.error = httpx.ConnectError("boom")
+ with pytest.raises(HTTPException) as exc:
+ asyncio.run(svc.fetch_rdap(kind="domain", target="example.com"))
+ assert exc.value.status_code == 502
+
+
+def test_fetch_rdap_invalid_json_maps_to_502(fake_http):
+ fake_http.response = _FakeResponse(200, None)
+ with pytest.raises(HTTPException) as exc:
+ asyncio.run(svc.fetch_rdap(kind="domain", target="example.com"))
+ assert exc.value.status_code == 502
diff --git a/apps/backend/tests/conftest.py b/apps/backend/tests/conftest.py
index 9e303a1b..104437e6 100644
--- a/apps/backend/tests/conftest.py
+++ b/apps/backend/tests/conftest.py
@@ -18,7 +18,6 @@
os.environ.setdefault("WEBAUTHN_ORIGINS", "http://localhost:3000")
os.environ.setdefault("WEBAUTHN_CHALLENGE_TTL_SECONDS", "300")
-from app.api.routes.workspaces.seed import ensure_system_org
from app.api.routes.workspaces.services import ensure_user_workspace_setup
from app.database import db_manager
from app.utils.collection_name import (
@@ -35,8 +34,6 @@
NOSQL_CONNECTIONS,
NOSQL_QUERY_HISTORY,
NOTES,
- ORGANIZATIONS,
- ORG_MEMBERSHIPS,
PASSWORD_ENTRIES,
PASSWORD_VAULTS,
PROJECTS,
@@ -50,7 +47,6 @@
WORKSPACE_MEMBERSHIPS,
URL_LINKS,
URL_CLICK_EVENTS,
- INVITATIONS,
)
pytest_plugins = ("pytest_asyncio",)
@@ -59,7 +55,7 @@
@pytest.fixture
async def clean_db():
"""Drop workspace-related collections and USERS before and after test."""
- collections = [API_CLIENT_COLLECTIONS, API_CLIENT_ENVIRONMENTS, API_CLIENT_HISTORY, API_CLIENT_WORKSPACES, API_KEY_VAULT_ENTRIES, ORGANIZATIONS, ORG_MEMBERSHIPS, WORKSPACES, WORKSPACE_MEMBERSHIPS, USERS, PASSWORD_ENTRIES, PASSWORD_VAULTS, NOTES, USER_PREFERENCES, BOOKMARKS, BOOKMARK_FOLDERS, TASKS, PROJECTS, ENV_MANAGER_ENTRIES, CODE_SNIPPETS, NOSQL_CONNECTIONS, NOSQL_QUERY_HISTORY, SQL_CONNECTIONS, S3_CONNECTIONS, REDIS_CONNECTIONS, URL_LINKS, URL_CLICK_EVENTS, JSON_FORMATTER_DOCUMENTS, INVITATIONS]
+ collections = [API_CLIENT_COLLECTIONS, API_CLIENT_ENVIRONMENTS, API_CLIENT_HISTORY, API_CLIENT_WORKSPACES, API_KEY_VAULT_ENTRIES, WORKSPACES, WORKSPACE_MEMBERSHIPS, USERS, PASSWORD_ENTRIES, PASSWORD_VAULTS, NOTES, USER_PREFERENCES, BOOKMARKS, BOOKMARK_FOLDERS, TASKS, PROJECTS, ENV_MANAGER_ENTRIES, CODE_SNIPPETS, NOSQL_CONNECTIONS, NOSQL_QUERY_HISTORY, SQL_CONNECTIONS, S3_CONNECTIONS, REDIS_CONNECTIONS, URL_LINKS, URL_CLICK_EVENTS, JSON_FORMATTER_DOCUMENTS]
# Clean before test
for coll in collections:
@@ -166,12 +162,6 @@ async def seed_legacy_user_data():
yield
-@pytest.fixture
-async def system_org_id():
- """Idempotently create the system org and return its id."""
- return await ensure_system_org()
-
-
@pytest.fixture
def personal_ws_for():
"""Return an async callable that creates (idempotently) a Personal workspace
diff --git a/apps/backend/tests/test_api_client_workspace_isolation.py b/apps/backend/tests/test_api_client_workspace_isolation.py
index a7d3105a..ca4685ce 100644
--- a/apps/backend/tests/test_api_client_workspace_isolation.py
+++ b/apps/backend/tests/test_api_client_workspace_isolation.py
@@ -15,10 +15,9 @@
from app.api.routes.workspaces.middleware import WorkspaceContext
-def _ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
+def _ctx(uid: str, ws_id: str) -> WorkspaceContext:
return WorkspaceContext(
uid=uid,
- org_id=org_id,
workspace_id=ws_id,
ws_role="admin",
is_personal=True,
@@ -28,15 +27,14 @@ def _ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
@pytest.mark.asyncio
async def test_api_client_collections_isolated_across_personal_workspaces(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
"""Collections created by u1 must not appear in u2's list."""
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
ws_u2 = await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
- ctx_u2 = _ctx("u2", ws_u2, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
+ ctx_u2 = _ctx("u2", ws_u2)
await api_client_svc.create_collection(ctx_u1, ApiClientCollectionCreate(name="u1-collection"))
@@ -49,32 +47,30 @@ async def test_api_client_collections_isolated_across_personal_workspaces(
@pytest.mark.asyncio
async def test_forged_workspace_cannot_read_api_client_collections(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
"""u2 forging u1's workspace_id still cannot see u1's collections due to owner_uid."""
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
await api_client_svc.create_collection(ctx_u1, ApiClientCollectionCreate(name="secret-collection"))
- forged_ctx = _ctx("u2", ws_u1, org_id) # u2 claims u1's workspace_id
+ forged_ctx = _ctx("u2", ws_u1) # u2 claims u1's workspace_id
cols = await api_client_svc.list_collections(ctx=forged_ctx)
assert cols == [] # owner_uid filter prevents cross-user bleed
@pytest.mark.asyncio
async def test_api_client_environments_and_history_isolated(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
"""Environments and history created by u1 must not appear for u2."""
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
ws_u2 = await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
- ctx_u2 = _ctx("u2", ws_u2, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
+ ctx_u2 = _ctx("u2", ws_u2)
await api_client_svc.create_environment(ctx_u1, ApiClientEnvironmentCreate(name="prod"))
await api_client_svc.create_history(
diff --git a/apps/backend/tests/test_api_key_vault_workspace_isolation.py b/apps/backend/tests/test_api_key_vault_workspace_isolation.py
index b6470095..017807e1 100644
--- a/apps/backend/tests/test_api_key_vault_workspace_isolation.py
+++ b/apps/backend/tests/test_api_key_vault_workspace_isolation.py
@@ -4,23 +4,22 @@
from app.api.routes.workspaces.middleware import WorkspaceContext
-def _ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
+def _ctx(uid: str, ws_id: str) -> WorkspaceContext:
return WorkspaceContext(
- uid=uid, org_id=org_id, workspace_id=ws_id, ws_role="admin",
+ uid=uid, workspace_id=ws_id, ws_role="admin",
is_personal=True, owner_uid=uid,
)
@pytest.mark.asyncio
async def test_api_key_vault_entries_are_isolated_across_personal_workspaces(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
ws_u2 = await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
- ctx_u2 = _ctx("u2", ws_u2, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
+ ctx_u2 = _ctx("u2", ws_u2)
await vault_svc.create_entry(ctx_u1, ApiKeyEntryCreate(encryptedData="enc-u1", iv="iv-u1"))
@@ -33,15 +32,14 @@ async def test_api_key_vault_entries_are_isolated_across_personal_workspaces(
@pytest.mark.asyncio
async def test_forged_workspace_id_cannot_cross_api_key_vault_data(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
await vault_svc.create_entry(ctx_u1, ApiKeyEntryCreate(encryptedData="enc-u1", iv="iv-u1"))
- forged_ctx = _ctx("u2", ws_u1, org_id) # u2 forges u1's workspace_id
+ forged_ctx = _ctx("u2", ws_u1) # u2 forges u1's workspace_id
entries = await vault_svc.list_entries(forged_ctx)
assert entries == [] # owner_uid filter saves us
diff --git a/apps/backend/tests/test_audit_middleware.py b/apps/backend/tests/test_audit_middleware.py
index 71415eee..fadbd61d 100644
--- a/apps/backend/tests/test_audit_middleware.py
+++ b/apps/backend/tests/test_audit_middleware.py
@@ -101,7 +101,7 @@ async def fake_insert_one(collection_name, data):
monkeypatch.setattr("app.api.routes.bookmarks.services.db_manager.insert_one", fake_insert_one)
_bm_ctx = WorkspaceContext(
- uid="uid1", org_id="org1", workspace_id="ws1", ws_role="admin",
+ uid="uid1", workspace_id="ws1", ws_role="admin",
is_personal=True, owner_uid="uid1",
)
diff --git a/apps/backend/tests/test_backfill.py b/apps/backend/tests/test_backfill.py
index e1a13a06..41e54505 100644
--- a/apps/backend/tests/test_backfill.py
+++ b/apps/backend/tests/test_backfill.py
@@ -1,26 +1,22 @@
import pytest
from app.api.routes.auth import users_repo
-from app.api.routes.workspaces.backfill import BACKFILL_COLLECTIONS, run_user_backfill
-from app.api.routes.workspaces.seed import ensure_system_org
+from app.api.routes.workspaces.backfill import run_user_backfill
from app.database import db_manager
from app.utils.collection_name import PASSWORD_ENTRIES, NOTES, USER_PREFERENCES
@pytest.mark.asyncio
async def test_backfill_stamps_legacy_rows(clean_db, seed_legacy_user_data):
- org_id = await ensure_system_org()
# seed_legacy_user_data inserts a few rows in PASSWORD_ENTRIES + NOTES
# for created_by="u1" with no workspace_id
ws_id = "ws-1"
- await run_user_backfill("u1", ws_id, org_id)
+ await run_user_backfill("u1", ws_id)
for entry in await db_manager.find(PASSWORD_ENTRIES, {"created_by": "u1"}, limit=100):
- assert entry["org_id"] == org_id
assert entry["workspace_id"] == ws_id
for note in await db_manager.find(NOTES, {"created_by": "u1"}, limit=100):
- assert note["org_id"] == org_id
assert note["workspace_id"] == ws_id
assert await users_repo.get_migrated_at("u1") is not None
@@ -28,25 +24,23 @@ async def test_backfill_stamps_legacy_rows(clean_db, seed_legacy_user_data):
@pytest.mark.asyncio
async def test_backfill_is_idempotent(clean_db, seed_legacy_user_data):
- org_id = await ensure_system_org()
ws_id = "ws-1"
- await run_user_backfill("u1", ws_id, org_id)
+ await run_user_backfill("u1", ws_id)
migrated_first = await users_repo.get_migrated_at("u1")
- await run_user_backfill("u1", ws_id, org_id)
+ await run_user_backfill("u1", ws_id)
migrated_second = await users_repo.get_migrated_at("u1")
assert migrated_first == migrated_second
@pytest.mark.asyncio
async def test_backfill_rewrites_pinned_tools(clean_db):
- org_id = await ensure_system_org()
ws_id = "ws-1"
await db_manager.insert_one(
USER_PREFERENCES,
{"_id": "u1", "uid": "u1", "toolFavorites": ["/app/passwords", "/app/notes"]},
)
- await run_user_backfill("u1", ws_id, org_id)
+ await run_user_backfill("u1", ws_id)
pref = await db_manager.find_one(USER_PREFERENCES, {"_id": "u1"})
assert pref["pinnedToolsByWorkspace"] == {ws_id: ["/app/passwords", "/app/notes"]}
diff --git a/apps/backend/tests/test_bookmarks_workspace_isolation.py b/apps/backend/tests/test_bookmarks_workspace_isolation.py
index b96cf227..5437cf4a 100644
--- a/apps/backend/tests/test_bookmarks_workspace_isolation.py
+++ b/apps/backend/tests/test_bookmarks_workspace_isolation.py
@@ -4,23 +4,22 @@
from app.api.routes.workspaces.middleware import WorkspaceContext
-def _ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
+def _ctx(uid: str, ws_id: str) -> WorkspaceContext:
return WorkspaceContext(
- uid=uid, org_id=org_id, workspace_id=ws_id, ws_role="admin",
+ uid=uid, workspace_id=ws_id, ws_role="admin",
is_personal=True, owner_uid=uid,
)
@pytest.mark.asyncio
async def test_bookmarks_are_isolated_across_personal_workspaces(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
ws_u2 = await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
- ctx_u2 = _ctx("u2", ws_u2, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
+ ctx_u2 = _ctx("u2", ws_u2)
await bm_svc.create_bookmark(ctx_u1, BookmarkCreate(title="u1 bookmark", url="https://u1.example"))
@@ -33,30 +32,28 @@ async def test_bookmarks_are_isolated_across_personal_workspaces(
@pytest.mark.asyncio
async def test_forged_workspace_id_cannot_cross_bookmark_data(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
await bm_svc.create_bookmark(ctx_u1, BookmarkCreate(title="u1 secret", url="https://u1.example"))
- forged_ctx = _ctx("u2", ws_u1, org_id) # u2 forges u1's workspace_id
+ forged_ctx = _ctx("u2", ws_u1) # u2 forges u1's workspace_id
bookmarks = await bm_svc.list_bookmarks(ctx=forged_ctx)
assert bookmarks == [] # owner_uid filter saves us
@pytest.mark.asyncio
async def test_folders_are_isolated_across_personal_workspaces(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
ws_u2 = await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
- ctx_u2 = _ctx("u2", ws_u2, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
+ ctx_u2 = _ctx("u2", ws_u2)
await bm_svc.create_folder(ctx_u1, BookmarkFolderCreate(name="u1 folder"))
diff --git a/apps/backend/tests/test_code_snippets_workspace_isolation.py b/apps/backend/tests/test_code_snippets_workspace_isolation.py
index b1d26c9d..989138d6 100644
--- a/apps/backend/tests/test_code_snippets_workspace_isolation.py
+++ b/apps/backend/tests/test_code_snippets_workspace_isolation.py
@@ -4,23 +4,22 @@
from app.api.routes.workspaces.middleware import WorkspaceContext
-def _ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
+def _ctx(uid: str, ws_id: str) -> WorkspaceContext:
return WorkspaceContext(
- uid=uid, org_id=org_id, workspace_id=ws_id, ws_role="admin",
+ uid=uid, workspace_id=ws_id, ws_role="admin",
is_personal=True, owner_uid=uid,
)
@pytest.mark.asyncio
async def test_code_snippets_are_isolated_across_personal_workspaces(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
ws_u2 = await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
- ctx_u2 = _ctx("u2", ws_u2, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
+ ctx_u2 = _ctx("u2", ws_u2)
await snippet_svc.create_code_snippet(ctx_u1, CodeSnippetCreate(title="snippet-u1", code="code-u1"))
@@ -33,15 +32,14 @@ async def test_code_snippets_are_isolated_across_personal_workspaces(
@pytest.mark.asyncio
async def test_forged_workspace_id_cannot_cross_code_snippets_data(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
await snippet_svc.create_code_snippet(ctx_u1, CodeSnippetCreate(title="snippet-u1", code="code-u1"))
- forged_ctx = _ctx("u2", ws_u1, org_id) # u2 forges u1's workspace_id
+ forged_ctx = _ctx("u2", ws_u1) # u2 forges u1's workspace_id
snippets = await snippet_svc.list_code_snippets(forged_ctx)
assert snippets == [] # owner_uid filter saves us
diff --git a/apps/backend/tests/test_dek_wrap.py b/apps/backend/tests/test_dek_wrap.py
deleted file mode 100644
index a31dec65..00000000
--- a/apps/backend/tests/test_dek_wrap.py
+++ /dev/null
@@ -1,64 +0,0 @@
-import pytest
-from httpx import AsyncClient
-
-
-@pytest.mark.asyncio
-async def test_get_dek_wrap_returns_null_initially(authed_client: AsyncClient, personal_ws_id: str):
- res = await authed_client.get(f"/api/v1/workspaces-api/workspaces/{personal_ws_id}/dek-wrap")
- assert res.status_code == 200
- body = res.json()
- assert body["wrappedDek"] is None
- assert body["wrappedDekVersion"] == 0
-
-
-@pytest.mark.asyncio
-async def test_post_then_get_wrap_roundtrip(authed_client: AsyncClient, personal_ws_id: str):
- # The authed_client uid is "test-uid" (from conftest), and "test-uid" is the
- # sole member of the personal workspace with ws_role="admin", so it can
- # wrap for itself.
- payload = {
- "target_uid": "test-uid",
- "wrapped": {"encrypted": "enc", "iv": "iv", "senderPublicKey": "pk"},
- }
- res = await authed_client.post(
- f"/api/v1/workspaces-api/workspaces/{personal_ws_id}/dek-wrap", json=payload,
- )
- assert res.status_code == 204
-
- get_res = await authed_client.get(
- f"/api/v1/workspaces-api/workspaces/{personal_ws_id}/dek-wrap",
- )
- assert get_res.status_code == 200
- body = get_res.json()
- assert body["wrappedDek"]["encrypted"] == "enc"
- assert body["wrappedDek"]["iv"] == "iv"
- assert body["wrappedDek"]["senderPublicKey"] == "pk"
- assert body["wrappedDekVersion"] == 1
-
-
-@pytest.mark.asyncio
-async def test_post_wrap_bumps_version_on_second_write(authed_client: AsyncClient, personal_ws_id: str):
- payload = {
- "target_uid": "test-uid",
- "wrapped": {"encrypted": "enc1", "iv": "iv1", "senderPublicKey": "pk1"},
- }
- res = await authed_client.post(
- f"/api/v1/workspaces-api/workspaces/{personal_ws_id}/dek-wrap", json=payload,
- )
- assert res.status_code == 204
-
- payload2 = {
- "target_uid": "test-uid",
- "wrapped": {"encrypted": "enc2", "iv": "iv2", "senderPublicKey": "pk2"},
- }
- res2 = await authed_client.post(
- f"/api/v1/workspaces-api/workspaces/{personal_ws_id}/dek-wrap", json=payload2,
- )
- assert res2.status_code == 204
-
- get_res = await authed_client.get(
- f"/api/v1/workspaces-api/workspaces/{personal_ws_id}/dek-wrap",
- )
- body = get_res.json()
- assert body["wrappedDek"]["encrypted"] == "enc2"
- assert body["wrappedDekVersion"] == 2
diff --git a/apps/backend/tests/test_email.py b/apps/backend/tests/test_email.py
deleted file mode 100644
index 2f66c9ea..00000000
--- a/apps/backend/tests/test_email.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import logging
-import pytest
-from app.core.email import send_invitation_email
-
-
-@pytest.mark.asyncio
-async def test_dev_mode_logs_when_key_missing(monkeypatch, caplog):
- monkeypatch.delenv("RESEND_API_KEY", raising=False)
- caplog.set_level(logging.INFO)
- await send_invitation_email(
- to="alice@example.com",
- token="tok-1",
- inviter_name="Bob",
- org_name="Acme",
- workspace_name="Prod",
- )
- assert any("alice@example.com" in r.message for r in caplog.records)
- assert any("tok-1" in r.message for r in caplog.records)
diff --git a/apps/backend/tests/test_environment_manager_workspace_isolation.py b/apps/backend/tests/test_environment_manager_workspace_isolation.py
index 69584a91..f4973f3d 100644
--- a/apps/backend/tests/test_environment_manager_workspace_isolation.py
+++ b/apps/backend/tests/test_environment_manager_workspace_isolation.py
@@ -4,23 +4,22 @@
from app.api.routes.workspaces.middleware import WorkspaceContext
-def _ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
+def _ctx(uid: str, ws_id: str) -> WorkspaceContext:
return WorkspaceContext(
- uid=uid, org_id=org_id, workspace_id=ws_id, ws_role="admin",
+ uid=uid, workspace_id=ws_id, ws_role="admin",
is_personal=True, owner_uid=uid,
)
@pytest.mark.asyncio
async def test_env_entries_are_isolated_across_personal_workspaces(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
ws_u2 = await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
- ctx_u2 = _ctx("u2", ws_u2, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
+ ctx_u2 = _ctx("u2", ws_u2)
await env_svc.create_entry(ctx_u1, EnvSetEntryCreate(encryptedData="enc-u1", iv="iv-u1"))
@@ -33,15 +32,14 @@ async def test_env_entries_are_isolated_across_personal_workspaces(
@pytest.mark.asyncio
async def test_forged_workspace_id_cannot_cross_env_data(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
await env_svc.create_entry(ctx_u1, EnvSetEntryCreate(encryptedData="enc-u1", iv="iv-u1"))
- forged_ctx = _ctx("u2", ws_u1, org_id) # u2 forges u1's workspace_id
+ forged_ctx = _ctx("u2", ws_u1) # u2 forges u1's workspace_id
entries = await env_svc.list_entries(forged_ctx)
assert entries == [] # owner_uid filter saves us
diff --git a/apps/backend/tests/test_invitations.py b/apps/backend/tests/test_invitations.py
deleted file mode 100644
index d81342bf..00000000
--- a/apps/backend/tests/test_invitations.py
+++ /dev/null
@@ -1,84 +0,0 @@
-import pytest
-from httpx import AsyncClient
-from app.utils.collection_name import INVITATIONS
-from app.api.routes.workspaces.invitations_repo import (
- create_invitation, find_invitation_by_token,
-)
-
-
-@pytest.mark.asyncio
-async def test_invitations_constant_exported():
- assert INVITATIONS == "invitations"
-
-
-@pytest.mark.asyncio
-async def test_create_and_find_invitation_roundtrip(clean_db):
- inv_id = await create_invitation({
- "_id": "inv-1",
- "org_id": "org-1",
- "workspace_id": None,
- "invited_email": "test@example.com",
- "invited_uid": None,
- "invited_role_org": "member",
- "invited_role_ws": None,
- "token": "tok-abc",
- "status": "pending",
- "invited_by": "u1",
- "created_at": 1000,
- "expires_at": 2000,
- })
- assert inv_id == "inv-1"
- doc = await find_invitation_by_token("tok-abc")
- assert doc is not None
- assert doc["status"] == "pending"
-
-
-@pytest.fixture
-async def authed_client_other_uid(clean_db) -> AsyncClient:
- """Authenticated HTTP client for a second test user (uid=other-uid)."""
- from app.api.routes.auth.services import get_current_uid
- from app.api.routes.workspaces.services import ensure_user_workspace_setup
- from app.main import app
-
- other_uid = "other-uid"
-
- def override_get_current_uid():
- return other_uid
-
- app.dependency_overrides[get_current_uid] = override_get_current_uid
-
- await ensure_user_workspace_setup(other_uid)
-
- async with AsyncClient(
- transport=__import__("httpx").ASGITransport(app=app),
- base_url="http://test",
- ) as client:
- yield client
-
- app.dependency_overrides.clear()
-
-
-@pytest.mark.asyncio
-async def test_invite_to_org_sends_email_and_creates_pending(authed_client, caplog):
- import logging
- caplog.set_level(logging.INFO)
- org_id = (await authed_client.post(
- "/api/v1/workspaces-api/orgs", json={"name": "InvOrg"}
- )).json()["id"]
- res = await authed_client.post(
- f"/api/v1/workspaces-api/orgs/{org_id}/members",
- json={"email": "alice@example.com", "role": "member"},
- )
- assert res.status_code == 201
- body = res.json()
- assert body["invited_email"] == "alice@example.com"
- assert body["status"] == "pending"
- # Dev-mode logger emits a line containing the recipient
- assert any("alice@example.com" in r.message for r in caplog.records)
-
-
-@pytest.mark.asyncio
-async def test_accept_invitation_creates_membership(authed_client_other_uid):
- # Setup: original user creates org + invite for the second user's email
- # then second user accepts. Implement via a separate authed_client for uid=alice.
- pass
diff --git a/apps/backend/tests/test_json_formatter_workspace_isolation.py b/apps/backend/tests/test_json_formatter_workspace_isolation.py
index 63d3c7cd..f486d55f 100644
--- a/apps/backend/tests/test_json_formatter_workspace_isolation.py
+++ b/apps/backend/tests/test_json_formatter_workspace_isolation.py
@@ -11,10 +11,9 @@
from app.api.routes.workspaces.middleware import WorkspaceContext
-def _ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
+def _ctx(uid: str, ws_id: str) -> WorkspaceContext:
return WorkspaceContext(
uid=uid,
- org_id=org_id,
workspace_id=ws_id,
ws_role="admin",
is_personal=True,
@@ -24,15 +23,14 @@ def _ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
@pytest.mark.asyncio
async def test_json_formatter_documents_isolated_across_personal_workspaces(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
"""Documents created by u1 must not appear in u2's list."""
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
ws_u2 = await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
- ctx_u2 = _ctx("u2", ws_u2, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
+ ctx_u2 = _ctx("u2", ws_u2)
# u1 creates a document
await jf_svc.create_document(ctx_u1, JsonFormatterDocumentCreate(title="test", pane="left", content="{}"))
@@ -49,17 +47,16 @@ async def test_json_formatter_documents_isolated_across_personal_workspaces(
@pytest.mark.asyncio
async def test_forged_workspace_cannot_read_json_formatter_documents(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
"""u2 forging u1's workspace_id still cannot see u1's documents due to owner_uid."""
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
await jf_svc.create_document(ctx_u1, JsonFormatterDocumentCreate(title="secret", pane="left", content="{}"))
# u2 forges u1's workspace_id
- forged_ctx = _ctx("u2", ws_u1, org_id)
+ forged_ctx = _ctx("u2", ws_u1)
docs = await jf_svc.list_documents(forged_ctx)
assert docs == [] # owner_uid filter prevents cross-user bleed
diff --git a/apps/backend/tests/test_member_publickeys.py b/apps/backend/tests/test_member_publickeys.py
deleted file mode 100644
index 8de7c81a..00000000
--- a/apps/backend/tests/test_member_publickeys.py
+++ /dev/null
@@ -1,15 +0,0 @@
-import pytest
-from httpx import AsyncClient
-
-
-@pytest.mark.asyncio
-async def test_list_member_publickeys_for_admin(authed_client: AsyncClient, personal_ws_id: str):
- res = await authed_client.get(
- f"/api/v1/workspaces-api/workspaces/{personal_ws_id}/member-publickeys",
- )
- assert res.status_code == 200
- members = res.json()
- assert len(members) >= 1
- assert members[0]["uid"] == "test-uid"
- # publicKey is None initially (keypair not generated)
- assert members[0]["publicKey"] is None
diff --git a/apps/backend/tests/test_members.py b/apps/backend/tests/test_members.py
deleted file mode 100644
index d304bcfe..00000000
--- a/apps/backend/tests/test_members.py
+++ /dev/null
@@ -1,36 +0,0 @@
-import pytest
-from httpx import AsyncClient
-
-
-@pytest.mark.asyncio
-async def test_list_org_members_after_create(authed_client: AsyncClient):
- org_id = (await authed_client.post(
- "/api/v1/workspaces-api/orgs", json={"name": "Acme"}
- )).json()["id"]
- res = await authed_client.get(f"/api/v1/workspaces-api/orgs/{org_id}/members")
- assert res.status_code == 200
- members = res.json()
- assert len(members) == 1
- assert members[0]["role"] == "owner"
-
-
-@pytest.mark.asyncio
-async def test_cannot_demote_sole_owner(authed_client: AsyncClient):
- org_id = (await authed_client.post(
- "/api/v1/workspaces-api/orgs", json={"name": "SoloOrg"}
- )).json()["id"]
- me_uid = "test-uid" # the authed_client fixture's uid
- res = await authed_client.patch(
- f"/api/v1/workspaces-api/orgs/{org_id}/members/{me_uid}",
- json={"role": "member"},
- )
- assert res.status_code == 400
- assert "sole owner" in res.json()["detail"].lower()
-
-
-@pytest.mark.asyncio
-async def test_remove_org_member_cascades_to_workspaces(authed_client: AsyncClient):
- # Multi-member tests need a second test uid — set up via fixtures.
- # Verifies that removing a user from the org also drops their ws memberships.
- # (Skip skeleton — implement once test fixtures support multi-user clients.)
- pass
diff --git a/apps/backend/tests/test_nosql_workspace_isolation.py b/apps/backend/tests/test_nosql_workspace_isolation.py
index 0acd025d..ade00d4e 100644
--- a/apps/backend/tests/test_nosql_workspace_isolation.py
+++ b/apps/backend/tests/test_nosql_workspace_isolation.py
@@ -4,23 +4,22 @@
from app.api.routes.workspaces.middleware import WorkspaceContext
-def _ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
+def _ctx(uid: str, ws_id: str) -> WorkspaceContext:
return WorkspaceContext(
- uid=uid, org_id=org_id, workspace_id=ws_id, ws_role="admin",
+ uid=uid, workspace_id=ws_id, ws_role="admin",
is_personal=True, owner_uid=uid,
)
@pytest.mark.asyncio
async def test_nosql_connections_are_isolated_across_personal_workspaces(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
ws_u2 = await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
- ctx_u2 = _ctx("u2", ws_u2, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
+ ctx_u2 = _ctx("u2", ws_u2)
await nosql_svc.create_connection(ctx_u1, ConnectionCreate(encryptedData="encrypted-u1", iv="iv-u1", name="conn-u1"))
@@ -33,15 +32,14 @@ async def test_nosql_connections_are_isolated_across_personal_workspaces(
@pytest.mark.asyncio
async def test_forged_workspace_id_cannot_cross_nosql_connections_data(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
await nosql_svc.create_connection(ctx_u1, ConnectionCreate(encryptedData="encrypted-u1", iv="iv-u1", name="conn-u1"))
- forged_ctx = _ctx("u2", ws_u1, org_id) # u2 forges u1's workspace_id
+ forged_ctx = _ctx("u2", ws_u1) # u2 forges u1's workspace_id
connections = await nosql_svc.list_connections(forged_ctx)
assert connections == [] # owner_uid filter saves us
diff --git a/apps/backend/tests/test_notes_workspace_isolation.py b/apps/backend/tests/test_notes_workspace_isolation.py
index 563dc486..55d8d651 100644
--- a/apps/backend/tests/test_notes_workspace_isolation.py
+++ b/apps/backend/tests/test_notes_workspace_isolation.py
@@ -4,23 +4,22 @@
from app.api.routes.workspaces.middleware import WorkspaceContext
-def _ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
+def _ctx(uid: str, ws_id: str) -> WorkspaceContext:
return WorkspaceContext(
- uid=uid, org_id=org_id, workspace_id=ws_id, ws_role="admin",
+ uid=uid, workspace_id=ws_id, ws_role="admin",
is_personal=True, owner_uid=uid,
)
@pytest.mark.asyncio
async def test_notes_are_isolated_across_personal_workspaces(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
ws_u2 = await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
- ctx_u2 = _ctx("u2", ws_u2, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
+ ctx_u2 = _ctx("u2", ws_u2)
await note_svc.create_note(ctx_u1, NoteCreate(title="u1 note"))
@@ -33,16 +32,15 @@ async def test_notes_are_isolated_across_personal_workspaces(
@pytest.mark.asyncio
async def test_forged_workspace_id_cannot_cross_note_data(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
await note_svc.create_note(ctx_u1, NoteCreate(title="u1 secret note"))
# u2 forges u1's workspace_id but has different uid -> owner_uid filter blocks them
- forged_ctx = _ctx("u2", ws_u1, org_id)
+ forged_ctx = _ctx("u2", ws_u1)
notes = await note_svc.list_notes(ctx=forged_ctx)
assert notes == []
diff --git a/apps/backend/tests/test_orgs_crud.py b/apps/backend/tests/test_orgs_crud.py
deleted file mode 100644
index 090277f9..00000000
--- a/apps/backend/tests/test_orgs_crud.py
+++ /dev/null
@@ -1,48 +0,0 @@
-import pytest
-from httpx import AsyncClient
-
-
-@pytest.mark.asyncio
-async def test_create_org_grants_owner_role(authed_client: AsyncClient):
- res = await authed_client.post(
- "/api/v1/workspaces-api/orgs",
- json={"name": "Acme Inc"},
- )
- assert res.status_code == 201
- body = res.json()
- assert body["name"] == "Acme Inc"
- assert body["slug"] == "acme-inc"
- assert body["kind"] == "user"
- assert body["org_role"] == "owner"
-
-
-@pytest.mark.asyncio
-async def test_rename_org_owner_only(authed_client: AsyncClient):
- create = await authed_client.post(
- "/api/v1/workspaces-api/orgs", json={"name": "Beta"}
- )
- org_id = create.json()["id"]
- res = await authed_client.patch(
- f"/api/v1/workspaces-api/orgs/{org_id}",
- json={"name": "Beta Renamed"},
- )
- assert res.status_code == 200
- assert res.json()["name"] == "Beta Renamed"
-
-
-@pytest.mark.asyncio
-async def test_delete_org_soft_deletes(authed_client: AsyncClient):
- create = await authed_client.post(
- "/api/v1/workspaces-api/orgs", json={"name": "Doomed"}
- )
- org_id = create.json()["id"]
- res = await authed_client.delete(f"/api/v1/workspaces-api/orgs/{org_id}")
- assert res.status_code == 204
- after = await authed_client.get("/api/v1/workspaces-api/orgs")
- assert all(o["id"] != org_id for o in after.json())
-
-
-@pytest.mark.asyncio
-async def test_cannot_delete_system_org(authed_client: AsyncClient, system_org_id: str):
- res = await authed_client.delete(f"/api/v1/workspaces-api/orgs/{system_org_id}")
- assert res.status_code == 403
diff --git a/apps/backend/tests/test_pending_wraps.py b/apps/backend/tests/test_pending_wraps.py
deleted file mode 100644
index 02df07a2..00000000
--- a/apps/backend/tests/test_pending_wraps.py
+++ /dev/null
@@ -1,40 +0,0 @@
-import pytest
-from httpx import AsyncClient
-
-
-@pytest.mark.asyncio
-async def test_pending_wraps_empty_initially(authed_client: AsyncClient, personal_ws_id: str):
- res = await authed_client.get(
- f"/api/v1/workspaces-api/workspaces/{personal_ws_id}/pending-wraps",
- )
- assert res.status_code == 200
- # The sole member (test-uid) has wrappedDek=None initially, so they appear
- # in the pending list. The personal workspace is bootstrapped with one
- # membership doc but no wrappedDek set yet.
- body = res.json()
- assert isinstance(body, list)
- # The bootstrapped member has no wrap yet, so they should appear.
- uids = [item["uid"] for item in body]
- assert "test-uid" in uids
-
-
-@pytest.mark.asyncio
-async def test_pending_wraps_disappears_after_wrap_posted(authed_client: AsyncClient, personal_ws_id: str):
- # Post a wrap for the sole member (test-uid).
- payload = {
- "target_uid": "test-uid",
- "wrapped": {"encrypted": "enc", "iv": "iv", "senderPublicKey": "pk"},
- }
- post_res = await authed_client.post(
- f"/api/v1/workspaces-api/workspaces/{personal_ws_id}/dek-wrap", json=payload,
- )
- assert post_res.status_code == 204
-
- # Now pending-wraps should not include test-uid.
- res = await authed_client.get(
- f"/api/v1/workspaces-api/workspaces/{personal_ws_id}/pending-wraps",
- )
- assert res.status_code == 200
- body = res.json()
- uids = [item["uid"] for item in body]
- assert "test-uid" not in uids
diff --git a/apps/backend/tests/test_rbac_dependency.py b/apps/backend/tests/test_rbac_dependency.py
index 215f5e7b..7f87df18 100644
--- a/apps/backend/tests/test_rbac_dependency.py
+++ b/apps/backend/tests/test_rbac_dependency.py
@@ -6,7 +6,7 @@
def _ctx(role: str, is_personal: bool = False) -> WorkspaceContext:
return WorkspaceContext(
- uid="u1", org_id="o1", workspace_id="w1",
+ uid="u1", workspace_id="w1",
ws_role=role, is_personal=is_personal,
owner_uid="u1" if is_personal else None,
)
diff --git a/apps/backend/tests/test_redis_commander_workspace_isolation.py b/apps/backend/tests/test_redis_commander_workspace_isolation.py
index 8618188f..fb71e02a 100644
--- a/apps/backend/tests/test_redis_commander_workspace_isolation.py
+++ b/apps/backend/tests/test_redis_commander_workspace_isolation.py
@@ -8,10 +8,9 @@
from app.api.routes.workspaces.middleware import WorkspaceContext
-def _ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
+def _ctx(uid: str, ws_id: str) -> WorkspaceContext:
return WorkspaceContext(
uid=uid,
- org_id=org_id,
workspace_id=ws_id,
ws_role="admin",
is_personal=True,
@@ -21,15 +20,14 @@ def _ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
@pytest.mark.asyncio
async def test_redis_connections_are_isolated_across_personal_workspaces(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
"""Connections created by u1 must not appear in u2's list."""
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
ws_u2 = await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
- ctx_u2 = _ctx("u2", ws_u2, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
+ ctx_u2 = _ctx("u2", ws_u2)
await redis_svc.create_connection(
ctx_u1, RedisConnectionCreate(encryptedData="encrypted-u1", iv="iv-u1", name="conn-u1")
@@ -44,18 +42,17 @@ async def test_redis_connections_are_isolated_across_personal_workspaces(
@pytest.mark.asyncio
async def test_forged_workspace_id_cannot_cross_redis_connections_data(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
"""u2 forging u1's workspace_id still cannot see u1's connections due to owner_uid."""
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
await redis_svc.create_connection(
ctx_u1, RedisConnectionCreate(encryptedData="encrypted-u1", iv="iv-u1", name="conn-u1")
)
- forged_ctx = _ctx("u2", ws_u1, org_id) # u2 forges u1's workspace_id
+ forged_ctx = _ctx("u2", ws_u1) # u2 forges u1's workspace_id
connections = await redis_svc.list_connections(forged_ctx)
assert connections == [] # owner_uid filter saves us
diff --git a/apps/backend/tests/test_s3_drive_workspace_isolation.py b/apps/backend/tests/test_s3_drive_workspace_isolation.py
index 5b7f6ec1..c7d4bb3a 100644
--- a/apps/backend/tests/test_s3_drive_workspace_isolation.py
+++ b/apps/backend/tests/test_s3_drive_workspace_isolation.py
@@ -8,10 +8,9 @@
from app.api.routes.workspaces.middleware import WorkspaceContext
-def _ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
+def _ctx(uid: str, ws_id: str) -> WorkspaceContext:
return WorkspaceContext(
uid=uid,
- org_id=org_id,
workspace_id=ws_id,
ws_role="admin",
is_personal=True,
@@ -21,15 +20,14 @@ def _ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
@pytest.mark.asyncio
async def test_s3_drive_connections_are_isolated_across_personal_workspaces(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
"""Connections created by u1 must not appear in u2's list."""
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
ws_u2 = await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
- ctx_u2 = _ctx("u2", ws_u2, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
+ ctx_u2 = _ctx("u2", ws_u2)
await s3_drive_svc.create_connection(
ctx_u1, S3ConnectionCreate(encryptedData="encrypted-u1", iv="iv-u1", name="conn-u1", provider="aws")
@@ -44,18 +42,17 @@ async def test_s3_drive_connections_are_isolated_across_personal_workspaces(
@pytest.mark.asyncio
async def test_forged_workspace_id_cannot_cross_s3_drive_connections_data(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
"""u2 forging u1's workspace_id still cannot see u1's connections due to owner_uid."""
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
await s3_drive_svc.create_connection(
ctx_u1, S3ConnectionCreate(encryptedData="encrypted-u1", iv="iv-u1", name="conn-u1", provider="aws")
)
- forged_ctx = _ctx("u2", ws_u1, org_id) # u2 forges u1's workspace_id
+ forged_ctx = _ctx("u2", ws_u1) # u2 forges u1's workspace_id
connections = await s3_drive_svc.list_connections(forged_ctx)
assert connections == [] # owner_uid filter saves us
diff --git a/apps/backend/tests/test_sql_client_workspace_isolation.py b/apps/backend/tests/test_sql_client_workspace_isolation.py
index c680980c..f4055697 100644
--- a/apps/backend/tests/test_sql_client_workspace_isolation.py
+++ b/apps/backend/tests/test_sql_client_workspace_isolation.py
@@ -8,10 +8,9 @@
from app.api.routes.workspaces.middleware import WorkspaceContext
-def _ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
+def _ctx(uid: str, ws_id: str) -> WorkspaceContext:
return WorkspaceContext(
uid=uid,
- org_id=org_id,
workspace_id=ws_id,
ws_role="admin",
is_personal=True,
@@ -21,15 +20,14 @@ def _ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
@pytest.mark.asyncio
async def test_sql_client_connections_are_isolated_across_personal_workspaces(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
"""Connections created by u1 must not appear in u2's list."""
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
ws_u2 = await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
- ctx_u2 = _ctx("u2", ws_u2, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
+ ctx_u2 = _ctx("u2", ws_u2)
await sql_client_svc.create_connection(
ctx_u1, SqlConnectionCreate(encryptedData="encrypted-u1", iv="iv-u1", name="conn-u1", type="postgresql")
@@ -44,18 +42,17 @@ async def test_sql_client_connections_are_isolated_across_personal_workspaces(
@pytest.mark.asyncio
async def test_forged_workspace_id_cannot_cross_sql_client_connections_data(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
"""u2 forging u1's workspace_id still cannot see u1's connections due to owner_uid."""
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
await sql_client_svc.create_connection(
ctx_u1, SqlConnectionCreate(encryptedData="encrypted-u1", iv="iv-u1", name="conn-u1", type="postgresql")
)
- forged_ctx = _ctx("u2", ws_u1, org_id) # u2 forges u1's workspace_id
+ forged_ctx = _ctx("u2", ws_u1) # u2 forges u1's workspace_id
connections = await sql_client_svc.list_connections(forged_ctx)
assert connections == [] # owner_uid filter saves us
diff --git a/apps/backend/tests/test_sweeper.py b/apps/backend/tests/test_sweeper.py
deleted file mode 100644
index 266c8ad8..00000000
--- a/apps/backend/tests/test_sweeper.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import pytest
-from app.api.routes.workspaces.sweeper import (
- SOFT_DELETE_RETENTION_MS, run_sweeper_once,
-)
-from app.api.routes.workspaces.repo import upsert_org, set_org_deleted, find_org
-from app.api.routes.workspaces.invitations_repo import create_invitation, find_invitation_by_token
-from app.utils.utils import create_timestamp
-
-
-@pytest.mark.asyncio
-async def test_sweeper_hard_deletes_old_orgs(clean_db):
- org_id = await upsert_org("Old", "old", "user", "u1")
- old_ts = create_timestamp() - SOFT_DELETE_RETENTION_MS - 1000
- await set_org_deleted(org_id, old_ts)
-
- fresh_id = await upsert_org("Fresh", "fresh", "user", "u1")
- await set_org_deleted(fresh_id, create_timestamp())
-
- stats = await run_sweeper_once()
- assert stats["orgs_hard_deleted"] == 1
- assert await find_org(org_id) is None
- assert await find_org(fresh_id) is not None # still in grace
-
-
-@pytest.mark.asyncio
-async def test_sweeper_expires_old_invitations(clean_db):
- old_ts = create_timestamp() - 1000
- await create_invitation({
- "_id": "inv-old", "org_id": "o1", "workspace_id": None,
- "invited_email": "x@y.com", "invited_uid": None,
- "invited_role_org": "member", "invited_role_ws": None,
- "token": "tok-old", "status": "pending",
- "invited_by": "u1",
- "created_at": old_ts - 10000, "expires_at": old_ts,
- })
- stats = await run_sweeper_once()
- assert stats["invitations_expired"] == 1
- doc = await find_invitation_by_token("tok-old")
- assert doc["status"] == "expired"
diff --git a/apps/backend/tests/test_tasks_workspace_isolation.py b/apps/backend/tests/test_tasks_workspace_isolation.py
index 6992eb87..ea29c2f4 100644
--- a/apps/backend/tests/test_tasks_workspace_isolation.py
+++ b/apps/backend/tests/test_tasks_workspace_isolation.py
@@ -4,32 +4,31 @@
from app.api.routes.workspaces.middleware import WorkspaceContext
-def _ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
+def _ctx(uid: str, ws_id: str) -> WorkspaceContext:
return WorkspaceContext(
- uid=uid, org_id=org_id, workspace_id=ws_id, ws_role="admin",
+ uid=uid, workspace_id=ws_id, ws_role="admin",
is_personal=True, owner_uid=uid,
)
-def _shared_ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
+def _shared_ctx(uid: str, ws_id: str) -> WorkspaceContext:
"""A shared (non-personal) workspace context — no owner_uid lock, so any
member with a writer role can mutate any task in the workspace."""
return WorkspaceContext(
- uid=uid, org_id=org_id, workspace_id=ws_id, ws_role="admin",
+ uid=uid, workspace_id=ws_id, ws_role="admin",
is_personal=False, owner_uid=None,
)
@pytest.mark.asyncio
async def test_tasks_are_isolated_across_personal_workspaces(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
ws_u2 = await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
- ctx_u2 = _ctx("u2", ws_u2, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
+ ctx_u2 = _ctx("u2", ws_u2)
await task_svc.create_task(ctx_u1, TaskCreate(text="u1 task"))
@@ -42,14 +41,13 @@ async def test_tasks_are_isolated_across_personal_workspaces(
@pytest.mark.asyncio
async def test_projects_are_isolated_across_personal_workspaces(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
ws_u2 = await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
- ctx_u2 = _ctx("u2", ws_u2, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
+ ctx_u2 = _ctx("u2", ws_u2)
await task_svc.create_project(ctx_u1, ProjectCreate(name="u1 project", color="#ff0000"))
@@ -62,31 +60,29 @@ async def test_projects_are_isolated_across_personal_workspaces(
@pytest.mark.asyncio
async def test_forged_workspace_id_cannot_cross_task_data(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
await task_svc.create_task(ctx_u1, TaskCreate(text="u1 secret task"))
# u2 forges u1's workspace_id but has different uid → owner_uid filter blocks them
- forged_ctx = _ctx("u2", ws_u1, org_id)
+ forged_ctx = _ctx("u2", ws_u1)
result = await task_svc.list_tasks(ctx=forged_ctx)
assert result.items == []
@pytest.mark.asyncio
async def test_shared_workspace_member_can_edit_anothers_task(
- clean_db, system_org_id,
+ clean_db,
):
"""Collaboration unlock: in a shared workspace, member B can edit / move /
delete a task member A created (was blocked by the old created_by filter)."""
- org_id = system_org_id
ws = "ws-shared-1"
- ctx_a = _shared_ctx("u1", ws, org_id)
- ctx_b = _shared_ctx("u2", ws, org_id)
+ ctx_a = _shared_ctx("u1", ws)
+ ctx_b = _shared_ctx("u2", ws)
created = await task_svc.create_task(ctx_a, TaskCreate(text="shared task"))
@@ -114,22 +110,20 @@ async def test_shared_workspace_member_can_edit_anothers_task(
@pytest.mark.asyncio
async def test_shared_workspace_isolated_from_other_shared_workspace(
- clean_db, system_org_id,
+ clean_db,
):
"""A shared workspace's tasks never leak into a different workspace."""
- org_id = system_org_id
- ctx_a = _shared_ctx("u1", "ws-shared-A", org_id)
- ctx_other = _shared_ctx("u1", "ws-shared-B", org_id)
+ ctx_a = _shared_ctx("u1", "ws-shared-A")
+ ctx_other = _shared_ctx("u1", "ws-shared-B")
await task_svc.create_task(ctx_a, TaskCreate(text="A-only"))
assert (await task_svc.list_tasks(ctx=ctx_other)).items == []
@pytest.mark.asyncio
-async def test_assignee_filter(clean_db, system_org_id):
+async def test_assignee_filter(clean_db):
"""list_tasks assignee filter supports me / unassigned / specific uid."""
- org_id = system_org_id
- ctx = _shared_ctx("u1", "ws-assignee", org_id)
+ ctx = _shared_ctx("u1", "ws-assignee")
await task_svc.create_task(ctx, TaskCreate(text="assigned to u2", assigneeUid="u2"))
await task_svc.create_task(ctx, TaskCreate(text="unassigned"))
diff --git a/apps/backend/tests/test_url_shortener_workspace_isolation.py b/apps/backend/tests/test_url_shortener_workspace_isolation.py
index 1ef937d3..cbcd18b0 100644
--- a/apps/backend/tests/test_url_shortener_workspace_isolation.py
+++ b/apps/backend/tests/test_url_shortener_workspace_isolation.py
@@ -12,10 +12,9 @@
from app.api.routes.workspaces.middleware import WorkspaceContext
-def _ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
+def _ctx(uid: str, ws_id: str) -> WorkspaceContext:
return WorkspaceContext(
uid=uid,
- org_id=org_id,
workspace_id=ws_id,
ws_role="admin",
is_personal=True,
@@ -25,15 +24,14 @@ def _ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
@pytest.mark.asyncio
async def test_url_shortener_links_isolated_across_personal_workspaces(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
"""Links created by u1 must not appear in u2's list."""
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
ws_u2 = await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
- ctx_u2 = _ctx("u2", ws_u2, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
+ ctx_u2 = _ctx("u2", ws_u2)
# u1 creates a link
await url_svc.create_link(ctx_u1, ShortLinkCreate(original_url="https://example.com"))
@@ -50,31 +48,29 @@ async def test_url_shortener_links_isolated_across_personal_workspaces(
@pytest.mark.asyncio
async def test_forged_workspace_cannot_read_url_shortener_links(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
"""u2 forging u1's workspace_id still cannot see u1's links due to owner_uid."""
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
await url_svc.create_link(ctx_u1, ShortLinkCreate(original_url="https://secret.com"))
# u2 forges u1's workspace_id
- forged_ctx = _ctx("u2", ws_u1, org_id)
+ forged_ctx = _ctx("u2", ws_u1)
links = await url_svc.list_my_short_urls(ctx=forged_ctx)
assert links == [] # owner_uid filter prevents cross-user bleed
@pytest.mark.asyncio
async def test_click_events_inherit_link_workspace_stamps(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
- """Click events must inherit the link's org_id, workspace_id, owner_uid stamps."""
- org_id = system_org_id
+ """Click events must inherit the link's workspace_id, owner_uid stamps."""
ws_u1 = await personal_ws_for("u1")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
# u1 creates a link
link = await url_svc.create_link(ctx_u1, ShortLinkCreate(original_url="https://example.com"))
@@ -90,7 +86,6 @@ async def test_click_events_inherit_link_workspace_stamps(
# Verify the link has the correct stamps
link_doc = await db_manager.find_one(COLLECTION, {"_id": code})
assert link_doc is not None
- assert link_doc["org_id"] == org_id
assert link_doc["workspace_id"] == ws_u1
assert link_doc["owner_uid"] == "u1"
@@ -98,6 +93,5 @@ async def test_click_events_inherit_link_workspace_stamps(
click_events = await db_manager.find(CLICKS_COLLECTION, {"code": code}, limit=10)
assert len(click_events) > 0
for event in click_events:
- assert event.get("org_id") == org_id
assert event.get("workspace_id") == ws_u1
assert event.get("owner_uid") == "u1"
diff --git a/apps/backend/tests/test_workspace_crud_b.py b/apps/backend/tests/test_workspace_crud_b.py
deleted file mode 100644
index e247f257..00000000
--- a/apps/backend/tests/test_workspace_crud_b.py
+++ /dev/null
@@ -1,64 +0,0 @@
-import pytest
-from httpx import AsyncClient
-
-
-@pytest.mark.asyncio
-async def test_create_shared_workspace(authed_client: AsyncClient):
- org_create = await authed_client.post(
- "/api/v1/workspaces-api/orgs", json={"name": "Acme"}
- )
- org_id = org_create.json()["id"]
- res = await authed_client.post(
- f"/api/v1/workspaces-api/orgs/{org_id}/workspaces",
- json={"name": "Production"},
- )
- assert res.status_code == 201
- body = res.json()
- assert body["name"] == "Production"
- assert body["is_personal"] is False
- assert body["kind"] == "shared"
- assert body["ws_role"] == "admin" # cascade from org owner
-
-
-@pytest.mark.asyncio
-async def test_personal_workspace_cannot_be_deleted(
- authed_client: AsyncClient, personal_ws_id: str,
-):
- res = await authed_client.delete(f"/api/v1/workspaces-api/workspaces/{personal_ws_id}")
- assert res.status_code == 403
-
-
-@pytest.mark.asyncio
-async def test_delete_shared_workspace(authed_client: AsyncClient):
- org_create = await authed_client.post(
- "/api/v1/workspaces-api/orgs", json={"name": "DoomedOrg"}
- )
- org_id = org_create.json()["id"]
- ws_create = await authed_client.post(
- f"/api/v1/workspaces-api/orgs/{org_id}/workspaces",
- json={"name": "Doomed"},
- )
- ws_id = ws_create.json()["id"]
- res = await authed_client.delete(f"/api/v1/workspaces-api/workspaces/{ws_id}")
- assert res.status_code == 204
- listing = await authed_client.get("/api/v1/workspaces-api/workspaces")
- assert all(w["id"] != ws_id for w in listing.json())
-
-
-@pytest.mark.asyncio
-async def test_rename_workspace(authed_client: AsyncClient):
- org_create = await authed_client.post(
- "/api/v1/workspaces-api/orgs", json={"name": "RenameOrg"}
- )
- org_id = org_create.json()["id"]
- ws_create = await authed_client.post(
- f"/api/v1/workspaces-api/orgs/{org_id}/workspaces",
- json={"name": "First"},
- )
- ws_id = ws_create.json()["id"]
- res = await authed_client.patch(
- f"/api/v1/workspaces-api/workspaces/{ws_id}",
- json={"name": "Second"},
- )
- assert res.status_code == 200
- assert res.json()["name"] == "Second"
diff --git a/apps/backend/tests/test_workspace_ctx.py b/apps/backend/tests/test_workspace_ctx.py
index 55dc5332..951e744b 100644
--- a/apps/backend/tests/test_workspace_ctx.py
+++ b/apps/backend/tests/test_workspace_ctx.py
@@ -8,8 +8,6 @@
get_workspace_ctx,
)
from app.api.routes.workspaces.repo import (
- upsert_org,
- upsert_org_membership,
upsert_personal_workspace,
upsert_ws_membership,
)
@@ -17,10 +15,8 @@
@pytest.mark.asyncio
async def test_get_workspace_ctx_rejects_non_member(make_request, clean_db):
- org_id = await upsert_org("Acme", "acme", "user", "owner-uid")
- await upsert_org_membership(org_id, "owner-uid", "owner")
- ws_id = await upsert_personal_workspace(org_id, "owner-uid")
- await upsert_ws_membership(ws_id, org_id, "owner-uid", "admin")
+ ws_id = await upsert_personal_workspace("owner-uid")
+ await upsert_ws_membership(ws_id, "owner-uid", "admin")
req = make_request(cookies={"active_workspace": ws_id})
with pytest.raises(HTTPException) as exc:
@@ -30,10 +26,8 @@ async def test_get_workspace_ctx_rejects_non_member(make_request, clean_db):
@pytest.mark.asyncio
async def test_default_personal_ws_id_returns_personal_ws(clean_db):
- org_id = await upsert_org("MyDevTools Cloud", "mydevtools-cloud", "system", None)
- await upsert_org_membership(org_id, "u1", "member")
- ws_id = await upsert_personal_workspace(org_id, "u1")
- await upsert_ws_membership(ws_id, org_id, "u1", "admin")
+ ws_id = await upsert_personal_workspace("u1")
+ await upsert_ws_membership(ws_id, "u1", "admin")
assert await default_personal_ws_id("u1") == ws_id
@@ -41,7 +35,6 @@ async def test_default_personal_ws_id_returns_personal_ws(clean_db):
def test_apply_workspace_filter_personal_adds_owner_uid():
ctx = WorkspaceContext(
uid="u1",
- org_id="o1",
workspace_id="w1",
ws_role="admin",
is_personal=True,
@@ -50,7 +43,6 @@ def test_apply_workspace_filter_personal_adds_owner_uid():
out = apply_workspace_filter(ctx, {"foo": "bar"})
assert out == {
"foo": "bar",
- "org_id": "o1",
"workspace_id": "w1",
"owner_uid": "u1",
}
@@ -59,7 +51,6 @@ def test_apply_workspace_filter_personal_adds_owner_uid():
def test_apply_legacy_or_filter_bounds_by_user_field():
ctx = WorkspaceContext(
uid="u1",
- org_id="o1",
workspace_id="w1",
ws_role="admin",
is_personal=True,
@@ -67,7 +58,7 @@ def test_apply_legacy_or_filter_bounds_by_user_field():
)
out = apply_legacy_or_filter(ctx, {"foo": "bar"}, user_field="created_by")
assert out["$or"] == [
- {"org_id": "o1", "workspace_id": "w1", "owner_uid": "u1"},
+ {"workspace_id": "w1", "owner_uid": "u1"},
{"workspace_id": {"$exists": False}, "created_by": "u1"},
]
assert out["foo"] == "bar"
@@ -76,7 +67,6 @@ def test_apply_legacy_or_filter_bounds_by_user_field():
def test_apply_legacy_or_filter_preserves_caller_or():
ctx = WorkspaceContext(
uid="u1",
- org_id="o1",
workspace_id="w1",
ws_role="admin",
is_personal=True,
@@ -88,21 +78,15 @@ def test_apply_legacy_or_filter_preserves_caller_or():
assert "$and" in out
assert out["$and"][0] == base
assert out["$and"][1]["$or"] == [
- {"org_id": "o1", "workspace_id": "w1", "owner_uid": "u1"},
+ {"workspace_id": "w1", "owner_uid": "u1"},
{"workspace_id": {"$exists": False}, "created_by": "u1"},
]
@pytest.mark.asyncio
-async def test_org_owner_gets_implicit_workspace_admin(clean_db, make_request):
- org_id = await upsert_org("Acme", "acme", "user", "owner-uid")
- await upsert_org_membership(org_id, "owner-uid", "owner")
- # Shared workspace (Personal for now, since shared CRUD lands in B5).
- ws_id = await upsert_personal_workspace(org_id, "owner-uid")
- # ONLY org membership exists. NO workspace membership for the owner.
- # ws membership exists for someone else.
- await upsert_org_membership(org_id, "member-uid", "member")
- await upsert_ws_membership(ws_id, org_id, "member-uid", "admin")
+async def test_admin_member_gets_workspace_ctx(clean_db, make_request):
+ ws_id = await upsert_personal_workspace("owner-uid")
+ await upsert_ws_membership(ws_id, "owner-uid", "admin")
req = make_request(cookies={"active_workspace": ws_id})
ctx = await get_workspace_ctx(req, uid="owner-uid")
@@ -111,12 +95,9 @@ async def test_org_owner_gets_implicit_workspace_admin(clean_db, make_request):
@pytest.mark.asyncio
-async def test_org_member_without_ws_membership_is_rejected(clean_db, make_request):
- org_id = await upsert_org("Acme2", "acme2", "user", "owner-uid")
- await upsert_org_membership(org_id, "owner-uid", "owner")
- await upsert_org_membership(org_id, "plain-member", "member")
- ws_id = await upsert_personal_workspace(org_id, "owner-uid")
- await upsert_ws_membership(ws_id, org_id, "owner-uid", "admin")
+async def test_non_member_without_ws_membership_is_rejected(clean_db, make_request):
+ ws_id = await upsert_personal_workspace("owner-uid")
+ await upsert_ws_membership(ws_id, "owner-uid", "admin")
req = make_request(cookies={"active_workspace": ws_id})
with pytest.raises(HTTPException) as exc:
diff --git a/apps/backend/tests/test_workspace_invariant.py b/apps/backend/tests/test_workspace_invariant.py
index fce4a555..37d1a12b 100644
--- a/apps/backend/tests/test_workspace_invariant.py
+++ b/apps/backend/tests/test_workspace_invariant.py
@@ -4,23 +4,22 @@
from app.api.routes.workspaces.middleware import WorkspaceContext
-def _ctx(uid: str, ws_id: str, org_id: str) -> WorkspaceContext:
+def _ctx(uid: str, ws_id: str) -> WorkspaceContext:
return WorkspaceContext(
- uid=uid, org_id=org_id, workspace_id=ws_id, ws_role="admin",
+ uid=uid, workspace_id=ws_id, ws_role="admin",
is_personal=True, owner_uid=uid,
)
@pytest.mark.asyncio
async def test_password_entries_are_isolated_across_personal_workspaces(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
ws_u2 = await personal_ws_for("u2")
- ctx_u1 = _ctx("u1", ws_u1, org_id)
- ctx_u2 = _ctx("u2", ws_u2, org_id)
+ ctx_u1 = _ctx("u1", ws_u1)
+ ctx_u2 = _ctx("u2", ws_u2)
await pw_svc.create_entry(
ctx_u1, PasswordEntryCreate(encryptedData="enc-u1", iv="iv-u1"),
@@ -35,17 +34,16 @@ async def test_password_entries_are_isolated_across_personal_workspaces(
@pytest.mark.asyncio
async def test_forged_workspace_id_cannot_cross_user_data(
- clean_db, system_org_id, personal_ws_for,
+ clean_db, personal_ws_for,
):
- org_id = system_org_id
ws_u1 = await personal_ws_for("u1")
await personal_ws_for("u2")
await pw_svc.create_entry(
- _ctx("u1", ws_u1, org_id),
+ _ctx("u1", ws_u1),
PasswordEntryCreate(encryptedData="enc-u1", iv="iv-u1"),
)
- forged_ctx = _ctx("u2", ws_u1, org_id) # u2 forges u1's workspace_id
+ forged_ctx = _ctx("u2", ws_u1) # u2 forges u1's workspace_id
entries = await pw_svc.list_entries(ctx=forged_ctx)
assert entries == [] # owner_uid filter saves us
diff --git a/apps/backend/tests/test_workspace_routes.py b/apps/backend/tests/test_workspace_routes.py
index c70f7baf..1275534a 100644
--- a/apps/backend/tests/test_workspace_routes.py
+++ b/apps/backend/tests/test_workspace_routes.py
@@ -2,16 +2,6 @@
from httpx import AsyncClient
-@pytest.mark.asyncio
-async def test_list_orgs_returns_mydevtools_cloud(authed_client: AsyncClient):
- res = await authed_client.get("/api/v1/workspaces-api/orgs")
- assert res.status_code == 200
- orgs = res.json()
- assert len(orgs) == 1
- assert orgs[0]["slug"] == "mydevtools-cloud"
- assert orgs[0]["org_role"] == "member"
-
-
@pytest.mark.asyncio
async def test_list_workspaces_returns_personal(authed_client: AsyncClient):
res = await authed_client.get("/api/v1/workspaces-api/workspaces")
diff --git a/apps/backend/tests/test_workspace_setup.py b/apps/backend/tests/test_workspace_setup.py
deleted file mode 100644
index 837ac1b0..00000000
--- a/apps/backend/tests/test_workspace_setup.py
+++ /dev/null
@@ -1,33 +0,0 @@
-import pytest
-from app.api.routes.workspaces.repo import (
- find_org_membership,
- find_ws_membership,
- find_user_workspaces,
-)
-from app.api.routes.workspaces.seed import ensure_system_org
-from app.api.routes.workspaces.services import ensure_user_workspace_setup
-
-
-@pytest.mark.asyncio
-async def test_first_call_creates_membership_and_personal_workspace(clean_db):
- org_id = await ensure_system_org()
- ws_id = await ensure_user_workspace_setup("u1")
-
- assert await find_org_membership(org_id, "u1") is not None
- assert await find_ws_membership(ws_id, "u1") is not None
-
- workspaces = await find_user_workspaces("u1")
- assert len(workspaces) == 1
- assert workspaces[0]["is_personal"] is True
- assert workspaces[0]["owner_uid"] == "u1"
- assert workspaces[0]["name"] == "Personal"
-
-
-@pytest.mark.asyncio
-async def test_second_call_is_no_op(clean_db, count_inserts):
- await ensure_system_org()
- await ensure_user_workspace_setup("u1")
- before = count_inserts()
- await ensure_user_workspace_setup("u1")
- after = count_inserts()
- assert after == before
diff --git a/apps/backend/tests/test_workspaces_seed.py b/apps/backend/tests/test_workspaces_seed.py
deleted file mode 100644
index 76b4c7a3..00000000
--- a/apps/backend/tests/test_workspaces_seed.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import pytest
-from app.api.routes.workspaces.seed import (
- SYSTEM_ORG_NAME,
- SYSTEM_ORG_SLUG,
- ensure_system_org,
- get_system_org_id,
-)
-from app.database import db_manager
-from app.utils.collection_name import ORGANIZATIONS
-
-
-@pytest.mark.asyncio
-async def test_ensure_system_org_creates_singleton(clean_db):
- org_id_first = await ensure_system_org()
- org_id_second = await ensure_system_org()
-
- assert org_id_first == org_id_second
-
- docs = await db_manager.find(
- ORGANIZATIONS, {"slug": SYSTEM_ORG_SLUG}, limit=10
- )
- assert len(docs) == 1
- assert docs[0]["name"] == SYSTEM_ORG_NAME
- assert docs[0]["kind"] == "system"
- assert docs[0].get("owner_uid") is None
-
-
-@pytest.mark.asyncio
-async def test_get_system_org_id_returns_none_when_not_seeded(clean_db):
- assert await get_system_org_id() is None
-
-
-@pytest.mark.asyncio
-async def test_get_system_org_id_returns_id_after_seed(clean_db):
- seeded = await ensure_system_org()
- fetched = await get_system_org_id()
- assert fetched == seeded
diff --git a/apps/desktop/src-tauri/src/router/workspaces.rs b/apps/desktop/src-tauri/src/router/workspaces.rs
index b0a8e078..a719ba63 100644
--- a/apps/desktop/src-tauri/src/router/workspaces.rs
+++ b/apps/desktop/src-tauri/src/router/workspaces.rs
@@ -1,7 +1,7 @@
//! Local mirror of FastAPI `/api/v1/workspaces-api/*`.
//!
-//! Offline there is exactly one org and one personal workspace, synthesized to
-//! match the shapes in apps/web/src/lib/workspace-api.ts.
+//! Offline there is exactly one personal workspace, synthesized to match the
+//! shapes in apps/web/src/lib/workspace-api.ts.
use serde_json::json;
@@ -9,23 +9,11 @@ use crate::error::Result;
use crate::router::ApiResponse;
use crate::state::AppState;
-pub const LOCAL_ORG_ID: &str = "local-org";
pub const LOCAL_WORKSPACE_ID: &str = "local-personal";
-fn local_org() -> serde_json::Value {
- json!({
- "id": LOCAL_ORG_ID,
- "name": "This Mac",
- "slug": "local",
- "kind": "user",
- "org_role": "owner",
- })
-}
-
fn local_workspace() -> serde_json::Value {
json!({
"id": LOCAL_WORKSPACE_ID,
- "org_id": LOCAL_ORG_ID,
"name": "Personal",
"slug": "personal-local",
"is_personal": true,
@@ -37,7 +25,6 @@ fn local_workspace() -> serde_json::Value {
pub fn handle(state: &AppState, method: &str, path: &str, body: Option<&str>) -> Result {
match (method, path) {
- ("GET", "/api/v1/workspaces-api/orgs") => ApiResponse::ok(&json!([local_org()])),
("GET", "/api/v1/workspaces-api/workspaces") => {
ApiResponse::ok(&json!([local_workspace()]))
}
diff --git a/apps/web/messages/af.json b/apps/web/messages/af.json
index 31e18388..404053fe 100644
--- a/apps/web/messages/af.json
+++ b/apps/web/messages/af.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Chmod-sakrekenaar",
+ "codeScreenshot": "Kode-skermkiekie",
+ "cssGenerators": "CSS-genereerders",
+ "exifViewer": "EXIF-kyker",
+ "faviconGenerator": "Favicon-genereerder",
+ "jsonDiff": "JSON-verskil",
+ "jsonpathPlayground": "JSONPath-speelperk",
+ "keycodeInspector": "Sleutelkode-inspekteur",
+ "markdownTableGenerator": "Markdown-tabel",
+ "timezoneConverter": "Tydsone-omskakelaar",
+ "tokenCounter": "Token-teller",
+ "webhookTester": "Webhook-toetser",
+ "websocketTester": "WebSocket-toetser",
+ "whoisLookup": "Whois-opsoek"
},
"Help": {
"title": "Hulp en dokumentasie",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Bcrypt-genereerder en -toetser",
+ "description": "Hash wagwoorde met bcrypt en verifieer bestaande hashe."
+ },
+ "chmodCalculator": {
+ "title": "Chmod-sakrekenaar",
+ "description": "Skakel Unix-regte om tussen oktaal, simbolies en merkblokkies."
+ },
+ "codeScreenshot": {
+ "title": "Kode-skermkiekie",
+ "description": "Verander bronkode in pragtige, deelbare beelde."
+ },
+ "cssGenerators": {
+ "title": "CSS-genereerders",
+ "description": "box-shadow-, border-radius- en vloeibare clamp()-genereerders."
+ },
+ "exifViewer": {
+ "title": "EXIF-kyker en -verwyderaar",
+ "description": "Inspekteer en verwyder foto-EXIF-metadata plaaslik."
+ },
+ "faviconGenerator": {
+ "title": "Favicon-genereerder",
+ "description": "Genereer favicons, app-ikone en manifest-snippers."
+ },
+ "jsonDiff": {
+ "title": "JSON-verskil",
+ "description": "Semantiese, struktuurbewuste verskil van twee JSON-dokumente."
+ },
+ "jsonpathPlayground": {
+ "title": "JSONPath-speelperk",
+ "description": "Bevraag JSON met JSONPath of JMESPath en sien lewendige resultate."
+ },
+ "keycodeInspector": {
+ "title": "Sleutelkode-inspekteur",
+ "description": "Druk 'n sleutel om sy JavaScript-gebeurteniskodes en wysigers te sien."
+ },
+ "markdownTableGenerator": {
+ "title": "Markdown-tabelgenereerder",
+ "description": "Bou Markdown- en HTML-tabelle in 'n sigblad-agtige roosterredigeerder."
+ },
+ "timezoneConverter": {
+ "title": "Tydsone-omskakelaar",
+ "description": "Skakel 'n tyd oor baie sones om met dagverskuiwing- en werksure-leidrade."
+ },
+ "tokenCounter": {
+ "title": "LLM-token-teller",
+ "description": "Tel LLM-tokens, visualiseer brokke en skat API-koste."
+ },
+ "webhookTester": {
+ "title": "Webhook-toetser",
+ "description": "Vang en inspekteer inkomende webhook-versoeke intyds."
+ },
+ "websocketTester": {
+ "title": "WebSocket-toetser",
+ "description": "Koppel aan WebSocket-eindpunte en ruil boodskappe intyds uit."
+ },
+ "whoisLookup": {
+ "title": "Whois-opsoek",
+ "description": "Soek domein- en IP-registrasiebesonderhede op via RDAP."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Bcrypt-genereerder en -toetser",
+ "subtitle": "Hash wagwoorde met bcrypt en verifieer hashe — alles bly in jou blaaier",
+ "tabs": {
+ "hash": "Hash",
+ "verify": "Verifieer"
+ },
+ "showPassword": "Wys wagwoord",
+ "hidePassword": "Versteek wagwoord",
+ "copy": "Kopieer",
+ "copied": "Gekopieer",
+ "hash": {
+ "passwordLabel": "Wagwoord",
+ "passwordPlaceholder": "Voer 'n wagwoord in om te hash…",
+ "costLabel": "Kostefaktor",
+ "costHint": "Hoër koste verdubbel die werk per stap. 10-12 is 'n algemene keuse vir produksie.",
+ "slowWarning": "Kostefaktore van 13 en hoër kan 'n paar sekondes neem om te bereken.",
+ "generate": "Genereer hash",
+ "generating": "Besig om te hash…",
+ "resultLabel": "Bcrypt-hash",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "Hash-anatomie",
+ "algorithm": "algoritme",
+ "cost": "koste",
+ "salt": "salt (22 karakters)",
+ "hash": "hash (31 karakters)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Wagwoord",
+ "passwordPlaceholder": "Voer die wagwoord in om te kontroleer…",
+ "hashLabel": "Bcrypt-hash",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Voer 'n wagwoord en 'n bcrypt-hash in om hulle te vergelyk.",
+ "invalidHash": "Dit lyk nie na 'n geldige bcrypt-hash nie.",
+ "checking": "Besig om te vergelyk…",
+ "match": "Pas — die wagwoord lewer hierdie hash.",
+ "noMatch": "Pas nie — die wagwoord lewer nie hierdie hash nie."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Chmod-sakrekenaar",
+ "subtitle": "Skakel Unix-lêerregte om tussen merkblokkies, oktaal en simboliese notasie",
+ "grid": {
+ "title": "Regte",
+ "read": "Lees",
+ "write": "Skryf",
+ "execute": "Voer uit",
+ "owner": "Eienaar",
+ "group": "Groep",
+ "others": "Ander"
+ },
+ "special": {
+ "title": "Spesiale bisse",
+ "setuid": "setuid",
+ "setgid": "setgid",
+ "sticky": "sticky"
+ },
+ "octal": {
+ "label": "Oktaal",
+ "placeholder": "755 of 4755",
+ "invalid": "Voer 3 of 4 oktale syfers in (0-7)."
+ },
+ "symbolic": {
+ "label": "Simbolies",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Voer 9 karakters in soos rwxr-xr-x (s/S, t/T toegelaat)."
+ },
+ "output": {
+ "title": "Opdrag",
+ "copy": "Kopieer",
+ "copied": "Gekopieer"
+ },
+ "explain": {
+ "line": "{cls} kan {perms}.",
+ "none": "{cls} het geen regte nie.",
+ "setuid": "setuid: die lêer loop met sy eienaar se voorregte.",
+ "setgid": "setgid: die lêer loop met sy groep se voorregte (nuwe lêers in 'n gids erf sy groep).",
+ "sticky": "sticky: net 'n lêer se eienaar kan dit binne hierdie gids uitvee of hernoem."
+ },
+ "presets": {
+ "title": "Algemene voorafinstellings",
+ "octal": "Oktaal",
+ "permissions": "Regte",
+ "description": "Tipiese gebruik",
+ "p644": "Gewone lêers: eienaar skryf, almal lees.",
+ "p755": "Uitvoerbares en gidse: almal kan lees en ingaan.",
+ "p600": "Privaat lêers: net die eienaar lees en skryf.",
+ "p700": "Privaat uitvoerbares en gidse: net eienaar.",
+ "p664": "Groep-skryfbare lêers.",
+ "p775": "Groep-skryfbare gidse en uitvoerbares.",
+ "p400": "Leesalleen vir die eienaar (bv. SSH-privaatsleutels)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Kode-skermkiekie",
+ "subtitle": "Verander kode in pragtige, deelbare beelde",
+ "languageLabel": "Taal",
+ "autoDetect": "Outo-bespeur",
+ "backgroundLabel": "Agtergrond",
+ "paddingLabel": "Opvulling",
+ "filenameLabel": "Lêernaam",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "Vensterkolletjies",
+ "themeLabel": "Venstertema",
+ "themeDark": "Donker",
+ "themeLight": "Lig",
+ "scaleLabel": "Uitvoerskaal",
+ "copyImage": "Kopieer beeld",
+ "copied": "Gekopieer",
+ "exportPng": "Voer PNG uit",
+ "inputLabel": "Kode",
+ "detected": "Bespeur: {language}",
+ "clear": "Vee uit",
+ "placeholder": "Plak jou kode hier…",
+ "previewLabel": "Voorskou",
+ "emptyPreview": "Jou kodevoorskou sal hier verskyn",
+ "exportFailed": "Kon nie beeld uitvoer nie",
+ "copyUnsupported": "Die kopiëring van beelde word nie in hierdie blaaier ondersteun nie",
+ "copyFailed": "Kon nie beeld kopieer nie",
+ "backgrounds": {
+ "ocean": "Oseaan",
+ "sunset": "Sonsondergang",
+ "candy": "Lekkers",
+ "forest": "Woud",
+ "midnight": "Middernag",
+ "gold": "Goud",
+ "aurora": "Aurora",
+ "slate": "Leiklip",
+ "transparent": "Deursigtig"
+ }
+ },
+ "CssGenerators": {
+ "title": "CSS-genereerders",
+ "subtitle": "box-shadow, border-radius en vloeibare clamp() — kopieer die CSS.",
+ "preview": "Voorskou",
+ "output": "CSS",
+ "copy": "Kopieer",
+ "copied": "Gekopieer",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "Vloeibare clamp()"
+ },
+ "boxShadow": {
+ "presets": "Voorafinstellings",
+ "layer": "Laag",
+ "addLayer": "Voeg laag by",
+ "removeLayer": "Verwyder laag",
+ "x": "X",
+ "y": "Y",
+ "blur": "Vervaag",
+ "spread": "Verspreiding",
+ "color": "Kleur",
+ "alpha": "Alfa",
+ "inset": "Inset",
+ "presetNames": {
+ "subtleCard": "Subtiele kaart",
+ "elevated": "Verhewe",
+ "hardOffset": "Harde verskuiwing",
+ "neonGlow": "Neon-gloed",
+ "inner": "Binne",
+ "layeredSmooth": "Gelaagd glad"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Modus",
+ "modes": {
+ "simple": "Per hoek",
+ "blob": "Organiese klont"
+ },
+ "unit": "Eenheid",
+ "linkCorners": "Koppel hoeke",
+ "randomize": "Verewekansig",
+ "horizontal": "Horisontale radiusse",
+ "vertical": "Vertikale radiusse",
+ "corners": {
+ "tl": "Bo links",
+ "tr": "Bo regs",
+ "br": "Onder regs",
+ "bl": "Onder links"
+ }
+ },
+ "clamp": {
+ "minFont": "Min lettertipe (px)",
+ "maxFont": "Maks lettertipe (px)",
+ "minViewport": "Min viewport (px)",
+ "maxViewport": "Maks viewport (px)",
+ "rootFont": "Wortel-lettertipe (px)",
+ "viewportWidth": "Gesimuleerde viewport",
+ "computedSize": "Berekende grootte",
+ "slope": "Helling",
+ "intercept": "Afsnit",
+ "previewText": "Voorbeeldteks vir voorskou",
+ "invalid": "Voer geldige, verskillende viewport-grense in."
+ }
+ },
+ "ExifViewer": {
+ "title": "EXIF-kyker en -verwyderaar",
+ "subtitle": "Inspekteer foto-metadata en verwyder dit in jou blaaier",
+ "dropTitle": "Los 'n beeld hier",
+ "dropHint": "of klik om te blaai — jou beeld verlaat nooit jou toestel nie",
+ "browse": "Kies beeld",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Besig om metadata te lees…",
+ "parseFailed": "Kon nie hierdie beeld lees nie",
+ "clear": "Verwyder beeld",
+ "gpsWarning": "Bevat GPS-ligging",
+ "openMap": "Bekyk op OpenStreetMap",
+ "noMetadata": "Geen ingebedde metadata is in hierdie beeld gevind nie.",
+ "facts": {
+ "name": "Naam",
+ "type": "Tipe",
+ "dimensions": "Afmetings",
+ "size": "Grootte"
+ },
+ "groups": {
+ "camera": "Kamera",
+ "exposure": "Blootstelling",
+ "timestamps": "Tydstempels",
+ "gps": "GPS-ligging",
+ "software": "Sagteware en Ander"
+ },
+ "gpsCard": {
+ "latitude": "Breedtegraad",
+ "longitude": "Lengtegraad",
+ "altitude": "Hoogte"
+ },
+ "raw": {
+ "title": "Alle metadata",
+ "copyJson": "Kopieer JSON",
+ "copied": "Gekopieer"
+ },
+ "remove": {
+ "title": "Verwyder metadata",
+ "jpegDescription": "Verwyder EXIF, XMP, IPTC en kommentaar sonder verlies en sonder om die beeld te herkomprimeer.",
+ "reencodeDescription": "Herkodeer die beeld om ingebedde metadata te verwyder. Dit herkomprimeer die pixels.",
+ "keepIcc": "Behou kleurprofiel (ICC)",
+ "stripButton": "Verwyder metadata",
+ "reencodeButton": "Herkodeer en verwyder",
+ "unsupported": "Metadata-verwydering word nie vir hierdie lêertipe ondersteun nie.",
+ "failed": "Kon nie metadata verwyder nie",
+ "before": "Voor",
+ "after": "Na",
+ "download": "Laai skoongemaakte beeld af"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Favicon-genereerder",
+ "subtitle": "Skep favicons en app-ikone uit een beeld",
+ "dropTitle": "Los 'n beeld hier",
+ "dropHint": "PNG, JPEG of SVG — klik om te blaai",
+ "browse": "Kies beeld",
+ "clear": "Verwyder beeld",
+ "backgroundLabel": "Agtergrond",
+ "transparent": "Deursigtig",
+ "solidColor": "Soliede kleur",
+ "cornerLabel": "Hoekvorm",
+ "cornerNone": "Vierkant",
+ "cornerRounded": "Afgerond",
+ "cornerCircle": "Sirkel",
+ "maskPaddingLabel": "Maskeerbare opvulling",
+ "maskable": "maskable",
+ "previewLabel": "Voorskou",
+ "copy": "Kopieer",
+ "headSnippet": "HTML head-snipper",
+ "downloadIco": "Laai favicon.ico af",
+ "downloadZip": "Laai alles af (.zip)"
+ },
+ "JsonDiff": {
+ "title": "JSON-verskil",
+ "subtitle": "Semantiese, struktuurbewuste vergelyking van twee JSON-dokumente — sleutelvolgorde en formatering word geïgnoreer",
+ "options": {
+ "ignoreArrayOrder": "Ignoreer skikkingsvolgorde"
+ },
+ "loadBothSamples": "Laai voorbeelde",
+ "sample": "Voorbeeld",
+ "format": "Formaat",
+ "clear": "Vee uit",
+ "formatError": "Kan nie formateer nie: invoer is nie geldige JSON nie",
+ "placeholder": "Plak of tik JSON hier…",
+ "panels": {
+ "left": "Oorspronklik (links)",
+ "right": "Gewysig (regs)",
+ "results": "Verskille"
+ },
+ "summary": {
+ "added": "{count} bygevoeg",
+ "removed": "{count} verwyder",
+ "changed": "{count} verander"
+ },
+ "types": {
+ "added": "Bygevoeg",
+ "removed": "Verwyder",
+ "changed": "Verander"
+ },
+ "copyChanges": "Kopieer as JSON",
+ "copied": "Gekopieer",
+ "empty": "Plak JSON in albei panele om hul verskille te sien.",
+ "identical": "Die twee dokumente is semanties identies."
+ },
+ "JsonpathPlayground": {
+ "title": "JSONPath-speelperk",
+ "subtitle": "Bevraag JSON-dokumente met JSONPath of JMESPath en sien lewendige resultate",
+ "engineLabel": "Enjin",
+ "queryLabel": "Navraag",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Kits-verwysing",
+ "cheats": {
+ "jpAllTitles": "Alle boektitels",
+ "jpAllAuthors": "Elke outeur, enige plek in die dokument",
+ "jpFirstBook": "Eerste boek",
+ "jpLastBook": "Laaste boek",
+ "jpSlice": "Eerste twee boeke (snit)",
+ "jpFilterPrice": "Boeke goedkoper as 40",
+ "jpFilterInStock": "Titels van boeke in voorraad",
+ "jpRecursive": "Alle waardes, rekursief",
+ "jmAllTitles": "Alle boektitels",
+ "jmFirstAuthor": "Outeur van die eerste boek",
+ "jmLastBook": "Laaste boek",
+ "jmSlice": "Eerste twee boeke (snit)",
+ "jmFilterPrice": "Titels van boeke goedkoper as 40",
+ "jmCount": "Tel boeke in voorraad",
+ "jmProjection": "Hervorm elke boek na 'n nuwe voorwerp",
+ "jmSort": "Titels gesorteer volgens prys"
+ },
+ "panels": {
+ "input": "JSON-invoer",
+ "results": "Resultate"
+ },
+ "sample": "Voorbeeld",
+ "format": "Formaat",
+ "clear": "Vee uit",
+ "formatError": "Kan nie formateer nie: invoer is nie geldige JSON nie",
+ "placeholder": "Plak of tik JSON hier…",
+ "outputPlaceholder": "Passings sal hier verskyn…",
+ "matchCount": "{count, plural, one {# passing} other {# passings}}",
+ "copy": "Kopieer",
+ "copied": "Gekopieer"
+ },
+ "KeycodeInspector": {
+ "title": "Sleutelkode-inspekteur",
+ "subtitle": "Druk enige sleutel om sy JavaScript-sleutelbordgebeurtenis te inspekteer",
+ "capture": {
+ "prompt": "Druk enige sleutel…",
+ "ready": "Luister — druk enige sleutel op jou sleutelbord",
+ "clickToFocus": "Klik hier om te fokus, druk dan enige sleutel"
+ },
+ "hero": {
+ "spaceKey": "(Spasie)",
+ "keyLabel": "event.key",
+ "repeat": "herhaal"
+ },
+ "copyJson": "Kopieer JSON",
+ "copiedJson": "Gebeurtenis-JSON na knipbord gekopieer",
+ "deprecated": "verouderd",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Standaard",
+ "left": "Links",
+ "right": "Regs",
+ "numpad": "Syferblok",
+ "unknown": "Onbekend"
+ },
+ "history": {
+ "title": "Geskiedenis",
+ "clear": "Vee geskiedenis uit",
+ "empty": "Sleuteldrukke sal hier verskyn."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Markdown-tabelgenereerder",
+ "subtitle": "Redigeer 'n rooster, plak uit 'n sigblad en kopieer 'n netjies uitgelynde Markdown- of HTML-tabel",
+ "addRow": "Voeg ry by",
+ "addColumn": "Voeg kolom by",
+ "importLabel": "Voer in",
+ "importPlaceholder": "Plak TSV (uit Excel / Google Sheets) of CSV hier…",
+ "importButton": "Voer in rooster in",
+ "pasteClipboard": "Plak uit knipbord",
+ "importEmpty": "Niks om in te voer nie — plak eers 'n paar rye",
+ "clipboardError": "Kon nie uit die knipbord lees nie",
+ "clear": "Vee uit",
+ "panels": {
+ "editor": "Tabelredigeerder",
+ "output": "Uitvoer"
+ },
+ "align": {
+ "left": "Lyn links",
+ "center": "Lyn middel",
+ "right": "Lyn regs"
+ },
+ "headerPlaceholder": "Opskrif",
+ "cellPlaceholder": "Sel",
+ "removeRow": "Verwyder ry",
+ "removeColumn": "Verwyder kolom",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Kopieer",
+ "copied": "Gekopieer"
+ },
+ "TimezoneConverter": {
+ "title": "Tydsone-omskakelaar",
+ "subtitle": "Skakel een oomblik oor die wêreld om vir vergaderingbeplanning.",
+ "dateTimeLabel": "Datum en tyd",
+ "now": "Nou",
+ "sourceZoneLabel": "Brontydsone",
+ "selectZone": "Kies sone",
+ "searchZone": "Soek tydsones...",
+ "noZone": "Geen tydsone gevind nie.",
+ "scrubLabel": "Verskuif tyd",
+ "reset": "Herstel",
+ "targetsLabel": "Teikensones",
+ "addZone": "Voeg sone by",
+ "remove": "Verwyder",
+ "localBadge": "Plaaslik",
+ "invalidDate": "Voer 'n geldige datum en tyd in.",
+ "bands": {
+ "work": "Werksure (09:00–18:00)",
+ "edge": "Randure (vroeg / aand)",
+ "night": "Nag"
+ }
+ },
+ "TokenCounter": {
+ "title": "LLM-token-teller",
+ "subtitle": "Tel tokens, visualiseer brokke en skat API-koste.",
+ "inputLabel": "Teks",
+ "sample": "Voorbeeld",
+ "clear": "Vee uit",
+ "placeholder": "Plak teks, kode of JSON om tokens te tel...",
+ "encoding": "Enkodering",
+ "loading": "Besig om tokeniseerder te laai...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / o-series)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Tokens",
+ "characters": "Karakters",
+ "words": "Woorde",
+ "ratio": "Tokens/woord",
+ "ratioSub": "ongeveer"
+ },
+ "claudeRow": "Claude (ongeveer)",
+ "estimate": "skatting",
+ "viz": {
+ "title": "Token-visualisering",
+ "toggle": "Wys",
+ "hint": "Wissel om elke token as 'n brok te kleur.",
+ "empty": "Voer teks in om tokens te visualiseer.",
+ "truncated": "Wys die eerste {limit} van {total} tokens."
+ },
+ "cost": {
+ "title": "Kosteberekenaar",
+ "disclaimer": "redigeerbare verstekwaardes — verifieer huidige pryse",
+ "model": "Model",
+ "expectedOutput": "Verwagte uitvoer-tokens",
+ "customInput": "Invoer $/MTok",
+ "customOutput": "Uitvoer $/MTok",
+ "inputCost": "Invoerkoste",
+ "outputCost": "Uitvoerkoste",
+ "total": "Totaal",
+ "per1000": "Per 1000 oproepe"
+ }
+ },
+ "WebhookTester": {
+ "title": "Webhook-toetser",
+ "subtitle": "Skep 'n tydelike URL en inspekteer inkomende webhook-versoeke lewendig.",
+ "intro": {
+ "title": "Skep 'n webhook-URL om versoeke te begin vang",
+ "hint": "Jy sal 'n onraaibare URL kry wat elke inkomende versoek opneem. Dit verval na 24 uur."
+ },
+ "createBtn": "Skep webhook-URL",
+ "creating": "Besig om te skep…",
+ "urlLabel": "Jou webhook-URL",
+ "expiresIn": "Verval oor {countdown}",
+ "copyUrlBtn": "Kopieer URL",
+ "copied": "Gekopieer",
+ "clearBtn": "Vee uit",
+ "newBinBtn": "Nuwe URL",
+ "requestsTitle": "Versoeke",
+ "selectPrompt": "Kies 'n versoek om sy besonderhede te inspekteer.",
+ "expiredNotice": "Hierdie webhook-URL het verval. Skep 'n nuwe een om aan te hou toets.",
+ "base64Badge": "base64",
+ "truncatedBadge": "afgekap",
+ "empty": {
+ "title": "Wag vir versoeke…",
+ "hint": "Stuur 'n versoek na jou URL en dit sal onmiddellik hier verskyn. Probeer dit:"
+ },
+ "detail": {
+ "receivedAt": "Ontvang",
+ "source": "Bron-IP",
+ "contentType": "Content-Type",
+ "query": "Navraagparameters",
+ "headers": "Kopstukke",
+ "body": "Liggaam",
+ "copyBody": "Kopieer liggaam",
+ "emptyBody": "Geen liggaam"
+ },
+ "errors": {
+ "createFailed": "Kon nie webhook-URL skep nie. Probeer asseblief weer.",
+ "clearFailed": "Kon nie versoeke uitvee nie. Probeer asseblief weer."
+ }
+ },
+ "WebsocketTester": {
+ "title": "WebSocket-toetser",
+ "subtitle": "Koppel aan 'n WebSocket-eindpunt en stuur, ontvang en inspekteer boodskappe lewendig",
+ "urlLabel": "Eindpunt-URL",
+ "protocolsLabel": "Subprotokolle",
+ "protocolsPlaceholder": "opsioneel, met kommas geskei",
+ "connect": "Koppel",
+ "disconnect": "Ontkoppel",
+ "status": {
+ "disconnected": "Ontkoppel",
+ "connecting": "Besig om te koppel",
+ "connected": "Gekoppel",
+ "error": "Fout"
+ },
+ "messages": {
+ "connecting": "Besig om aan {url} te koppel…",
+ "connected": "Verbinding gevestig",
+ "connectedWithProtocol": "Verbinding gevestig (protokol: {protocol})",
+ "invalidUrl": "Ongeldige URL — moet met ws:// of wss:// begin",
+ "connectFailed": "Kon nie verbinding oopmaak nie",
+ "socketError": "'n Verbindingsfout het voorgekom",
+ "notConnected": "Nie gekoppel nie — koppel eers",
+ "binaryMessage": "'n Binêre boodskap ontvang"
+ },
+ "log": {
+ "title": "Boodskaplog",
+ "empty": "Nog geen boodskappe nie. Koppel en begin stuur.",
+ "copyAll": "Kopieer alles",
+ "download": "Laai af as .txt",
+ "clear": "Vee log uit",
+ "prettyPrint": "Formateer JSON",
+ "showRaw": "Wys rou teks"
+ },
+ "composer": {
+ "placeholder": "Tik 'n boodskap om te stuur…",
+ "send": "Stuur",
+ "formatJson": "Formateer as JSON",
+ "invalidJson": "Boodskap is nie geldige JSON nie",
+ "hint": "Cmd/Ctrl+Enter om te stuur"
+ },
+ "ping": {
+ "label": "Outo-ping",
+ "seconds": "sek",
+ "intervalTitle": "Ping-interval in sekondes",
+ "payloadPlaceholder": "ping-loonvrag"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Whois-opsoek",
+ "subtitle": "Soek registrasiebesonderhede vir enige domein of IP-adres op via RDAP",
+ "placeholder": "example.com of 8.8.8.8",
+ "lookupBtn": "Soek op",
+ "looking": "Besig om op te soek…",
+ "refreshBtn": "Verfris",
+ "tryLabel": "Probeer:",
+ "copy": "Kopieer",
+ "copied": "Gekopieer",
+ "notAvailable": "Nie beskikbaar nie",
+ "summary": {
+ "registrar": "Registrateur",
+ "network": "Netwerk"
+ },
+ "dates": {
+ "created": "Geskep",
+ "updated": "Bygewerk",
+ "expires": "Verval"
+ },
+ "sections": {
+ "status": "Status",
+ "nameservers": "Naambedieners",
+ "contacts": "Kontakte",
+ "rawRdap": "Rou RDAP"
+ },
+ "contacts": {
+ "roles": "Rolle",
+ "name": "Naam",
+ "organization": "Organisasie"
+ },
+ "errors": {
+ "invalidTarget": "Voer 'n geldige domeinnaam of IP-adres in",
+ "lookupFailed": "Opsoek het misluk (status {status})"
+ }
}
}
diff --git a/apps/web/messages/ar.json b/apps/web/messages/ar.json
index 92258f15..4fe066b8 100644
--- a/apps/web/messages/ar.json
+++ b/apps/web/messages/ar.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "حاسبة Chmod",
+ "codeScreenshot": "لقطة شاشة للكود",
+ "cssGenerators": "مولّدات CSS",
+ "exifViewer": "عارض EXIF",
+ "faviconGenerator": "مولّد Favicon",
+ "jsonDiff": "مقارنة JSON",
+ "jsonpathPlayground": "ساحة JSONPath",
+ "keycodeInspector": "فاحص أكواد المفاتيح",
+ "markdownTableGenerator": "جدول Markdown",
+ "timezoneConverter": "محوّل المناطق الزمنية",
+ "tokenCounter": "عدّاد الرموز",
+ "webhookTester": "مختبِر Webhook",
+ "websocketTester": "مختبِر WebSocket",
+ "whoisLookup": "بحث Whois"
},
"Help": {
"title": "المساعدة والتوثيق",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "مولّد ومدقّق Bcrypt",
+ "description": "شفّر كلمات المرور باستخدام bcrypt وتحقّق من التجزئات الموجودة."
+ },
+ "chmodCalculator": {
+ "title": "حاسبة Chmod",
+ "description": "حوّل أذونات يونكس بين الثماني والرمزي ومربّعات الاختيار."
+ },
+ "codeScreenshot": {
+ "title": "لقطة شاشة للكود",
+ "description": "حوّل الكود المصدري إلى صور جميلة قابلة للمشاركة."
+ },
+ "cssGenerators": {
+ "title": "مولّدات CSS",
+ "description": "مولّدات box-shadow وborder-radius وclamp() المرن."
+ },
+ "exifViewer": {
+ "title": "عارض ومزيل EXIF",
+ "description": "افحص بيانات EXIF الوصفية للصور واحذفها محليًا."
+ },
+ "faviconGenerator": {
+ "title": "مولّد Favicon",
+ "description": "أنشئ أيقونات المفضلة وأيقونات التطبيق ومقتطفات manifest."
+ },
+ "jsonDiff": {
+ "title": "مقارنة JSON",
+ "description": "مقارنة دلالية واعية بالبنية بين مستندَي JSON."
+ },
+ "jsonpathPlayground": {
+ "title": "ساحة JSONPath",
+ "description": "استعلم عن JSON باستخدام JSONPath أو JMESPath وشاهد النتائج مباشرة."
+ },
+ "keycodeInspector": {
+ "title": "فاحص أكواد المفاتيح",
+ "description": "اضغط مفتاحًا لرؤية أكواد أحداث JavaScript ومفاتيح التعديل."
+ },
+ "markdownTableGenerator": {
+ "title": "مولّد جداول Markdown",
+ "description": "أنشئ جداول Markdown وHTML في محرّر شبكي يشبه جداول البيانات."
+ },
+ "timezoneConverter": {
+ "title": "محوّل المناطق الزمنية",
+ "description": "حوّل وقتًا عبر مناطق عديدة مع مؤشرات إزاحة اليوم وساعات العمل."
+ },
+ "tokenCounter": {
+ "title": "عدّاد رموز LLM",
+ "description": "احسب رموز LLM، وتصوّر الأجزاء، وقدّر تكلفة الـ API."
+ },
+ "webhookTester": {
+ "title": "مختبِر Webhook",
+ "description": "التقط وافحص طلبات webhook الواردة في الوقت الفعلي."
+ },
+ "websocketTester": {
+ "title": "مختبِر WebSocket",
+ "description": "اتصل بنقاط نهاية WebSocket وتبادل الرسائل في الوقت الفعلي."
+ },
+ "whoisLookup": {
+ "title": "بحث Whois",
+ "description": "ابحث عن تفاصيل تسجيل النطاق وعنوان IP عبر RDAP."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "مولّد ومدقّق Bcrypt",
+ "subtitle": "شفّر كلمات المرور باستخدام bcrypt وتحقّق من التجزئات — كل شيء يبقى في متصفّحك",
+ "tabs": {
+ "hash": "تجزئة",
+ "verify": "تحقّق"
+ },
+ "showPassword": "إظهار كلمة المرور",
+ "hidePassword": "إخفاء كلمة المرور",
+ "copy": "نسخ",
+ "copied": "تم النسخ",
+ "hash": {
+ "passwordLabel": "كلمة المرور",
+ "passwordPlaceholder": "أدخل كلمة مرور لتجزئتها…",
+ "costLabel": "عامل التكلفة",
+ "costHint": "التكلفة الأعلى تضاعف العمل في كل خطوة. عادةً يُستخدم 10-12 في الإنتاج.",
+ "slowWarning": "عوامل التكلفة 13 فأعلى قد تستغرق عدة ثوانٍ للحساب.",
+ "generate": "توليد التجزئة",
+ "generating": "جارٍ التجزئة…",
+ "resultLabel": "تجزئة Bcrypt",
+ "timeTaken": "{ms} مللي ثانية",
+ "anatomy": {
+ "title": "بنية التجزئة",
+ "algorithm": "الخوارزمية",
+ "cost": "التكلفة",
+ "salt": "الملح (22 حرفًا)",
+ "hash": "التجزئة (31 حرفًا)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "كلمة المرور",
+ "passwordPlaceholder": "أدخل كلمة المرور للتحقّق…",
+ "hashLabel": "تجزئة Bcrypt",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "أدخل كلمة مرور وتجزئة bcrypt لمقارنتهما.",
+ "invalidHash": "لا يبدو هذا تجزئة bcrypt صالحة.",
+ "checking": "جارٍ المقارنة…",
+ "match": "تطابق — كلمة المرور تنتج هذه التجزئة.",
+ "noMatch": "لا تطابق — كلمة المرور لا تنتج هذه التجزئة."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "حاسبة Chmod",
+ "subtitle": "حوّل أذونات ملفات يونكس بين مربّعات الاختيار والترميز الثماني والرمزي",
+ "grid": {
+ "title": "الأذونات",
+ "read": "قراءة",
+ "write": "كتابة",
+ "execute": "تنفيذ",
+ "owner": "المالك",
+ "group": "المجموعة",
+ "others": "الآخرون"
+ },
+ "special": {
+ "title": "البتات الخاصة",
+ "setuid": "setuid",
+ "setgid": "setgid",
+ "sticky": "sticky"
+ },
+ "octal": {
+ "label": "ثماني",
+ "placeholder": "755 أو 4755",
+ "invalid": "أدخل 3 أو 4 أرقام ثمانية (0-7)."
+ },
+ "symbolic": {
+ "label": "رمزي",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "أدخل 9 أحرف مثل rwxr-xr-x (يُسمح بـ s/S وt/T)."
+ },
+ "output": {
+ "title": "الأمر",
+ "copy": "نسخ",
+ "copied": "تم النسخ"
+ },
+ "explain": {
+ "line": "{cls} يمكنه {perms}.",
+ "none": "{cls} ليس لديه أي أذونات.",
+ "setuid": "setuid: يعمل الملف بصلاحيات مالكه.",
+ "setgid": "setgid: يعمل الملف بصلاحيات مجموعته (ترث الملفات الجديدة في مجلد مجموعته).",
+ "sticky": "sticky: يمكن فقط لمالك الملف حذفه أو إعادة تسميته داخل هذا المجلد."
+ },
+ "presets": {
+ "title": "الإعدادات الشائعة",
+ "octal": "ثماني",
+ "permissions": "الأذونات",
+ "description": "الاستخدام النموذجي",
+ "p644": "الملفات العادية: المالك يكتب، والجميع يقرأ.",
+ "p755": "الملفات القابلة للتنفيذ والمجلدات: يمكن للجميع القراءة والدخول.",
+ "p600": "الملفات الخاصة: المالك فقط يقرأ ويكتب.",
+ "p700": "الملفات القابلة للتنفيذ والمجلدات الخاصة: المالك فقط.",
+ "p664": "ملفات قابلة للكتابة من المجموعة.",
+ "p775": "مجلدات وملفات قابلة للتنفيذ وقابلة للكتابة من المجموعة.",
+ "p400": "قراءة فقط للمالك (مثل مفاتيح SSH الخاصة)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "لقطة شاشة للكود",
+ "subtitle": "حوّل الكود إلى صور جميلة قابلة للمشاركة",
+ "languageLabel": "اللغة",
+ "autoDetect": "كشف تلقائي",
+ "backgroundLabel": "الخلفية",
+ "paddingLabel": "الحشوة",
+ "filenameLabel": "اسم الملف",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "نقاط النافذة",
+ "themeLabel": "سمة النافذة",
+ "themeDark": "داكن",
+ "themeLight": "فاتح",
+ "scaleLabel": "مقياس التصدير",
+ "copyImage": "نسخ الصورة",
+ "copied": "تم النسخ",
+ "exportPng": "تصدير PNG",
+ "inputLabel": "الكود",
+ "detected": "المكتشَف: {language}",
+ "clear": "مسح",
+ "placeholder": "الصق الكود هنا…",
+ "previewLabel": "معاينة",
+ "emptyPreview": "ستظهر معاينة الكود هنا",
+ "exportFailed": "فشل تصدير الصورة",
+ "copyUnsupported": "نسخ الصور غير مدعوم في هذا المتصفّح",
+ "copyFailed": "فشل نسخ الصورة",
+ "backgrounds": {
+ "ocean": "محيط",
+ "sunset": "غروب",
+ "candy": "حلوى",
+ "forest": "غابة",
+ "midnight": "منتصف الليل",
+ "gold": "ذهبي",
+ "aurora": "شفق",
+ "slate": "رمادي",
+ "transparent": "شفاف"
+ }
+ },
+ "CssGenerators": {
+ "title": "مولّدات CSS",
+ "subtitle": "box-shadow وborder-radius وclamp() المرن — انسخ الـ CSS.",
+ "preview": "معاينة",
+ "output": "CSS",
+ "copy": "نسخ",
+ "copied": "تم النسخ",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "clamp() المرن"
+ },
+ "boxShadow": {
+ "presets": "الإعدادات المسبقة",
+ "layer": "طبقة",
+ "addLayer": "إضافة طبقة",
+ "removeLayer": "إزالة الطبقة",
+ "x": "X",
+ "y": "Y",
+ "blur": "تمويه",
+ "spread": "امتداد",
+ "color": "اللون",
+ "alpha": "ألفا",
+ "inset": "داخلي",
+ "presetNames": {
+ "subtleCard": "بطاقة خفيفة",
+ "elevated": "مرتفع",
+ "hardOffset": "إزاحة حادة",
+ "neonGlow": "توهّج نيون",
+ "inner": "داخلي",
+ "layeredSmooth": "متعدّد الطبقات ناعم"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "الوضع",
+ "modes": {
+ "simple": "لكل زاوية",
+ "blob": "شكل عضوي"
+ },
+ "unit": "الوحدة",
+ "linkCorners": "ربط الزوايا",
+ "randomize": "عشوائي",
+ "horizontal": "أنصاف الأقطار الأفقية",
+ "vertical": "أنصاف الأقطار الرأسية",
+ "corners": {
+ "tl": "أعلى اليسار",
+ "tr": "أعلى اليمين",
+ "br": "أسفل اليمين",
+ "bl": "أسفل اليسار"
+ }
+ },
+ "clamp": {
+ "minFont": "أدنى خط (px)",
+ "maxFont": "أقصى خط (px)",
+ "minViewport": "أدنى منفذ عرض (px)",
+ "maxViewport": "أقصى منفذ عرض (px)",
+ "rootFont": "خط الجذر (px)",
+ "viewportWidth": "منفذ عرض محاكى",
+ "computedSize": "الحجم المحسوب",
+ "slope": "الميل",
+ "intercept": "التقاطع",
+ "previewText": "نصّ معاينة للخط",
+ "invalid": "أدخل حدود منفذ عرض صالحة ومختلفة."
+ }
+ },
+ "ExifViewer": {
+ "title": "عارض ومزيل EXIF",
+ "subtitle": "افحص بيانات الصور الوصفية واحذفها في متصفّحك",
+ "dropTitle": "أفلِت صورة هنا",
+ "dropHint": "أو انقر للتصفّح — صورتك لا تغادر جهازك أبدًا",
+ "browse": "اختر صورة",
+ "formats": "JPEG وPNG وWebP وHEIC وTIFF",
+ "parsing": "جارٍ قراءة البيانات الوصفية…",
+ "parseFailed": "فشل قراءة هذه الصورة",
+ "clear": "إزالة الصورة",
+ "gpsWarning": "يحتوي على موقع GPS",
+ "openMap": "عرض على OpenStreetMap",
+ "noMetadata": "لم يُعثر على بيانات وصفية مضمّنة في هذه الصورة.",
+ "facts": {
+ "name": "الاسم",
+ "type": "النوع",
+ "dimensions": "الأبعاد",
+ "size": "الحجم"
+ },
+ "groups": {
+ "camera": "الكاميرا",
+ "exposure": "التعريض",
+ "timestamps": "الطوابع الزمنية",
+ "gps": "موقع GPS",
+ "software": "البرامج وأخرى"
+ },
+ "gpsCard": {
+ "latitude": "خط العرض",
+ "longitude": "خط الطول",
+ "altitude": "الارتفاع"
+ },
+ "raw": {
+ "title": "كل البيانات الوصفية",
+ "copyJson": "نسخ JSON",
+ "copied": "تم النسخ"
+ },
+ "remove": {
+ "title": "إزالة البيانات الوصفية",
+ "jpegDescription": "أزِل EXIF وXMP وIPTC والتعليقات بلا فقدان دون إعادة ضغط الصورة.",
+ "reencodeDescription": "أعِد ترميز الصورة لإزالة البيانات الوصفية المضمّنة. هذا يعيد ضغط البكسلات.",
+ "keepIcc": "الاحتفاظ بملف الألوان (ICC)",
+ "stripButton": "إزالة البيانات الوصفية",
+ "reencodeButton": "إعادة الترميز والإزالة",
+ "unsupported": "إزالة البيانات الوصفية غير مدعومة لهذا النوع من الملفات.",
+ "failed": "فشلت إزالة البيانات الوصفية",
+ "before": "قبل",
+ "after": "بعد",
+ "download": "تنزيل الصورة المنظّفة"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "مولّد Favicon",
+ "subtitle": "أنشئ أيقونات المفضلة وأيقونات التطبيق من صورة واحدة",
+ "dropTitle": "أفلِت صورة هنا",
+ "dropHint": "PNG أو JPEG أو SVG — انقر للتصفّح",
+ "browse": "اختر صورة",
+ "clear": "إزالة الصورة",
+ "backgroundLabel": "الخلفية",
+ "transparent": "شفاف",
+ "solidColor": "لون خالص",
+ "cornerLabel": "شكل الزاوية",
+ "cornerNone": "مربّع",
+ "cornerRounded": "مستدير",
+ "cornerCircle": "دائرة",
+ "maskPaddingLabel": "حشوة قابلة للإخفاء",
+ "maskable": "قابلة للإخفاء",
+ "previewLabel": "معاينة",
+ "copy": "نسخ",
+ "headSnippet": "مقتطف HTML head",
+ "downloadIco": "تنزيل favicon.ico",
+ "downloadZip": "تنزيل الكل (.zip)"
+ },
+ "JsonDiff": {
+ "title": "مقارنة JSON",
+ "subtitle": "مقارنة دلالية واعية بالبنية بين مستندَي JSON — يُتجاهل ترتيب المفاتيح والتنسيق",
+ "options": {
+ "ignoreArrayOrder": "تجاهل ترتيب المصفوفة"
+ },
+ "loadBothSamples": "تحميل الأمثلة",
+ "sample": "مثال",
+ "format": "تنسيق",
+ "clear": "مسح",
+ "formatError": "تعذّر التنسيق: الإدخال ليس JSON صالحًا",
+ "placeholder": "الصق أو اكتب JSON هنا…",
+ "panels": {
+ "left": "الأصلي (يسار)",
+ "right": "المعدّل (يمين)",
+ "results": "الاختلافات"
+ },
+ "summary": {
+ "added": "{count} مُضاف",
+ "removed": "{count} محذوف",
+ "changed": "{count} متغيّر"
+ },
+ "types": {
+ "added": "مُضاف",
+ "removed": "محذوف",
+ "changed": "متغيّر"
+ },
+ "copyChanges": "نسخ كـ JSON",
+ "copied": "تم النسخ",
+ "empty": "الصق JSON في كلا اللوحين لرؤية اختلافاتهما.",
+ "identical": "المستندان متطابقان دلاليًا."
+ },
+ "JsonpathPlayground": {
+ "title": "ساحة JSONPath",
+ "subtitle": "استعلم عن مستندات JSON باستخدام JSONPath أو JMESPath وشاهد النتائج مباشرة",
+ "engineLabel": "المحرّك",
+ "queryLabel": "الاستعلام",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "ورقة مرجعية",
+ "cheats": {
+ "jpAllTitles": "كل عناوين الكتب",
+ "jpAllAuthors": "كل مؤلّف في أي مكان بالمستند",
+ "jpFirstBook": "أول كتاب",
+ "jpLastBook": "آخر كتاب",
+ "jpSlice": "أول كتابين (شريحة)",
+ "jpFilterPrice": "الكتب الأرخص من 40",
+ "jpFilterInStock": "عناوين الكتب المتوفّرة",
+ "jpRecursive": "كل القيم، تعاوديًا",
+ "jmAllTitles": "كل عناوين الكتب",
+ "jmFirstAuthor": "مؤلّف أول كتاب",
+ "jmLastBook": "آخر كتاب",
+ "jmSlice": "أول كتابين (شريحة)",
+ "jmFilterPrice": "عناوين الكتب الأرخص من 40",
+ "jmCount": "عدّ الكتب المتوفّرة",
+ "jmProjection": "أعِد تشكيل كل كتاب إلى كائن جديد",
+ "jmSort": "العناوين مرتّبة حسب السعر"
+ },
+ "panels": {
+ "input": "إدخال JSON",
+ "results": "النتائج"
+ },
+ "sample": "مثال",
+ "format": "تنسيق",
+ "clear": "مسح",
+ "formatError": "تعذّر التنسيق: الإدخال ليس JSON صالحًا",
+ "placeholder": "الصق أو اكتب JSON هنا…",
+ "outputPlaceholder": "ستظهر التطابقات هنا…",
+ "matchCount": "{count, plural, zero {# تطابق} one {# تطابق} two {# تطابقان} few {# تطابقات} many {# تطابقًا} other {# تطابق}}",
+ "copy": "نسخ",
+ "copied": "تم النسخ"
+ },
+ "KeycodeInspector": {
+ "title": "فاحص أكواد المفاتيح",
+ "subtitle": "اضغط أي مفتاح لفحص حدث لوحة مفاتيح JavaScript الخاص به",
+ "capture": {
+ "prompt": "اضغط أي مفتاح…",
+ "ready": "يستمع — اضغط أي مفتاح على لوحة مفاتيحك",
+ "clickToFocus": "انقر هنا للتركيز، ثم اضغط أي مفتاح"
+ },
+ "hero": {
+ "spaceKey": "(مسافة)",
+ "keyLabel": "event.key",
+ "repeat": "تكرار"
+ },
+ "copyJson": "نسخ JSON",
+ "copiedJson": "تم نسخ JSON الحدث إلى الحافظة",
+ "deprecated": "مهمَل",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "قياسي",
+ "left": "يسار",
+ "right": "يمين",
+ "numpad": "لوحة أرقام",
+ "unknown": "غير معروف"
+ },
+ "history": {
+ "title": "السجل",
+ "clear": "مسح السجل",
+ "empty": "ستظهر ضغطات المفاتيح هنا."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "مولّد جداول Markdown",
+ "subtitle": "حرّر شبكة، والصق من جدول بيانات، وانسخ جدول Markdown أو HTML بمحاذاة أنيقة",
+ "addRow": "إضافة صف",
+ "addColumn": "إضافة عمود",
+ "importLabel": "استيراد",
+ "importPlaceholder": "الصق TSV (من Excel / Google Sheets) أو CSV هنا…",
+ "importButton": "استيراد إلى الشبكة",
+ "pasteClipboard": "لصق من الحافظة",
+ "importEmpty": "لا شيء للاستيراد — الصق بعض الصفوف أولًا",
+ "clipboardError": "تعذّرت القراءة من الحافظة",
+ "clear": "مسح",
+ "panels": {
+ "editor": "محرّر الجدول",
+ "output": "الناتج"
+ },
+ "align": {
+ "left": "محاذاة لليسار",
+ "center": "محاذاة للوسط",
+ "right": "محاذاة لليمين"
+ },
+ "headerPlaceholder": "رأس",
+ "cellPlaceholder": "خلية",
+ "removeRow": "إزالة الصف",
+ "removeColumn": "إزالة العمود",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "نسخ",
+ "copied": "تم النسخ"
+ },
+ "TimezoneConverter": {
+ "title": "محوّل المناطق الزمنية",
+ "subtitle": "حوّل لحظة واحدة عبر العالم لتخطيط الاجتماعات.",
+ "dateTimeLabel": "التاريخ والوقت",
+ "now": "الآن",
+ "sourceZoneLabel": "المنطقة الزمنية المصدر",
+ "selectZone": "اختر منطقة",
+ "searchZone": "ابحث في المناطق الزمنية...",
+ "noZone": "لم يُعثر على منطقة زمنية.",
+ "scrubLabel": "إزاحة الوقت",
+ "reset": "إعادة تعيين",
+ "targetsLabel": "المناطق الهدف",
+ "addZone": "إضافة منطقة",
+ "remove": "إزالة",
+ "localBadge": "محلي",
+ "invalidDate": "أدخل تاريخًا ووقتًا صالحين.",
+ "bands": {
+ "work": "ساعات العمل (09:00–18:00)",
+ "edge": "الساعات الهامشية (الصباح الباكر / المساء)",
+ "night": "الليل"
+ }
+ },
+ "TokenCounter": {
+ "title": "عدّاد رموز LLM",
+ "subtitle": "احسب الرموز، وتصوّر الأجزاء، وقدّر تكلفة الـ API.",
+ "inputLabel": "النص",
+ "sample": "مثال",
+ "clear": "مسح",
+ "placeholder": "الصق نصًا أو كودًا أو JSON لعدّ الرموز...",
+ "encoding": "الترميز",
+ "loading": "جارٍ تحميل المُرمِّز...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / o-series)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "الرموز",
+ "characters": "الأحرف",
+ "words": "الكلمات",
+ "ratio": "رموز/كلمة",
+ "ratioSub": "تقريبي"
+ },
+ "claudeRow": "Claude (تقريبي)",
+ "estimate": "تقدير",
+ "viz": {
+ "title": "تصوّر الرموز",
+ "toggle": "إظهار",
+ "hint": "بدّل لتلوين كل رمز كجزء.",
+ "empty": "أدخل نصًا لتصوّر الرموز.",
+ "truncated": "عرض أول {limit} من {total} رمزًا."
+ },
+ "cost": {
+ "title": "حاسبة التكلفة",
+ "disclaimer": "قيم افتراضية قابلة للتعديل — تحقّق من الأسعار الحالية",
+ "model": "النموذج",
+ "expectedOutput": "رموز الإخراج المتوقّعة",
+ "customInput": "إدخال $/MTok",
+ "customOutput": "إخراج $/MTok",
+ "inputCost": "تكلفة الإدخال",
+ "outputCost": "تكلفة الإخراج",
+ "total": "الإجمالي",
+ "per1000": "لكل 1000 طلب"
+ }
+ },
+ "WebhookTester": {
+ "title": "مختبِر Webhook",
+ "subtitle": "أنشئ عنوان URL مؤقّتًا وافحص طلبات webhook الواردة مباشرة.",
+ "intro": {
+ "title": "أنشئ عنوان webhook لبدء التقاط الطلبات",
+ "hint": "ستحصل على عنوان URL يصعب تخمينه يسجّل كل طلب وارد. تنتهي صلاحيته بعد 24 ساعة."
+ },
+ "createBtn": "إنشاء عنوان webhook",
+ "creating": "جارٍ الإنشاء…",
+ "urlLabel": "عنوان webhook الخاص بك",
+ "expiresIn": "تنتهي الصلاحية خلال {countdown}",
+ "copyUrlBtn": "نسخ العنوان",
+ "copied": "تم النسخ",
+ "clearBtn": "مسح",
+ "newBinBtn": "عنوان جديد",
+ "requestsTitle": "الطلبات",
+ "selectPrompt": "اختر طلبًا لفحص تفاصيله.",
+ "expiredNotice": "انتهت صلاحية عنوان webhook هذا. أنشئ عنوانًا جديدًا لمواصلة الاختبار.",
+ "base64Badge": "base64",
+ "truncatedBadge": "مقتطع",
+ "empty": {
+ "title": "بانتظار الطلبات…",
+ "hint": "أرسل طلبًا إلى عنوانك وسيظهر هنا فورًا. جرّب هذا:"
+ },
+ "detail": {
+ "receivedAt": "استُلم",
+ "source": "IP المصدر",
+ "contentType": "Content-Type",
+ "query": "معاملات الاستعلام",
+ "headers": "الترويسات",
+ "body": "المحتوى",
+ "copyBody": "نسخ المحتوى",
+ "emptyBody": "لا يوجد محتوى"
+ },
+ "errors": {
+ "createFailed": "فشل إنشاء عنوان webhook. يُرجى المحاولة مجددًا.",
+ "clearFailed": "فشل مسح الطلبات. يُرجى المحاولة مجددًا."
+ }
+ },
+ "WebsocketTester": {
+ "title": "مختبِر WebSocket",
+ "subtitle": "اتصل بنقطة نهاية WebSocket وأرسِل واستقبِل وافحص الرسائل مباشرة",
+ "urlLabel": "عنوان نقطة النهاية",
+ "protocolsLabel": "البروتوكولات الفرعية",
+ "protocolsPlaceholder": "اختياري، مفصولة بفواصل",
+ "connect": "اتصال",
+ "disconnect": "قطع الاتصال",
+ "status": {
+ "disconnected": "غير متصل",
+ "connecting": "جارٍ الاتصال",
+ "connected": "متصل",
+ "error": "خطأ"
+ },
+ "messages": {
+ "connecting": "جارٍ الاتصال بـ {url}…",
+ "connected": "تم تأسيس الاتصال",
+ "connectedWithProtocol": "تم تأسيس الاتصال (البروتوكول: {protocol})",
+ "invalidUrl": "عنوان URL غير صالح — يجب أن يبدأ بـ ws:// أو wss://",
+ "connectFailed": "فشل فتح الاتصال",
+ "socketError": "حدث خطأ في الاتصال",
+ "notConnected": "غير متصل — اتصل أولًا",
+ "binaryMessage": "تم استلام رسالة ثنائية"
+ },
+ "log": {
+ "title": "سجل الرسائل",
+ "empty": "لا رسائل بعد. اتصل وابدأ الإرسال.",
+ "copyAll": "نسخ الكل",
+ "download": "تنزيل كـ .txt",
+ "clear": "مسح السجل",
+ "prettyPrint": "تنسيق JSON",
+ "showRaw": "إظهار النص الخام"
+ },
+ "composer": {
+ "placeholder": "اكتب رسالة للإرسال…",
+ "send": "إرسال",
+ "formatJson": "تنسيق كـ JSON",
+ "invalidJson": "الرسالة ليست JSON صالحًا",
+ "hint": "Cmd/Ctrl+Enter للإرسال"
+ },
+ "ping": {
+ "label": "ping تلقائي",
+ "seconds": "ثانية",
+ "intervalTitle": "فترة ping بالثواني",
+ "payloadPlaceholder": "حمولة ping"
+ }
+ },
+ "WhoisLookup": {
+ "title": "بحث Whois",
+ "subtitle": "ابحث عن تفاصيل التسجيل لأي نطاق أو عنوان IP عبر RDAP",
+ "placeholder": "example.com أو 8.8.8.8",
+ "lookupBtn": "بحث",
+ "looking": "جارٍ البحث…",
+ "refreshBtn": "تحديث",
+ "tryLabel": "جرّب:",
+ "copy": "نسخ",
+ "copied": "تم النسخ",
+ "notAvailable": "غير متوفّر",
+ "summary": {
+ "registrar": "المسجِّل",
+ "network": "الشبكة"
+ },
+ "dates": {
+ "created": "أُنشئ",
+ "updated": "حُدّث",
+ "expires": "ينتهي"
+ },
+ "sections": {
+ "status": "الحالة",
+ "nameservers": "خوادم الأسماء",
+ "contacts": "جهات الاتصال",
+ "rawRdap": "RDAP الخام"
+ },
+ "contacts": {
+ "roles": "الأدوار",
+ "name": "الاسم",
+ "organization": "المؤسسة"
+ },
+ "errors": {
+ "invalidTarget": "أدخل اسم نطاق أو عنوان IP صالحًا",
+ "lookupFailed": "فشل البحث (الحالة {status})"
+ }
}
}
diff --git a/apps/web/messages/ca.json b/apps/web/messages/ca.json
index b31c412d..6395b095 100644
--- a/apps/web/messages/ca.json
+++ b/apps/web/messages/ca.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Calculadora Chmod",
+ "codeScreenshot": "Captura de codi",
+ "cssGenerators": "Generadors CSS",
+ "exifViewer": "Visualitzador EXIF",
+ "faviconGenerator": "Generador de favicon",
+ "jsonDiff": "Diff JSON",
+ "jsonpathPlayground": "Playground JSONPath",
+ "keycodeInspector": "Inspector de keycode",
+ "markdownTableGenerator": "Taula Markdown",
+ "timezoneConverter": "Convertidor de zona horària",
+ "tokenCounter": "Comptador de tokens",
+ "webhookTester": "Provador de webhooks",
+ "websocketTester": "Provador de WebSocket",
+ "whoisLookup": "Consulta Whois"
},
"Help": {
"title": "Ajuda i documentació",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Generador i verificador Bcrypt",
+ "description": "Genera hash de contrasenyes amb bcrypt i verifica hashes existents."
+ },
+ "chmodCalculator": {
+ "title": "Calculadora Chmod",
+ "description": "Converteix permisos Unix entre octal, simbòlic i caselles de selecció."
+ },
+ "codeScreenshot": {
+ "title": "Captura de codi",
+ "description": "Converteix codi font en imatges boniques i compartibles."
+ },
+ "cssGenerators": {
+ "title": "Generadors CSS",
+ "description": "Generadors de box-shadow, border-radius i clamp() fluid."
+ },
+ "exifViewer": {
+ "title": "Visualitzador i eliminador EXIF",
+ "description": "Inspecciona i elimina les metadades EXIF de les fotos localment."
+ },
+ "faviconGenerator": {
+ "title": "Generador de favicon",
+ "description": "Genera favicons, icones d'app i fragments de manifest."
+ },
+ "jsonDiff": {
+ "title": "Diff JSON",
+ "description": "Diff semàntic i conscient de l'estructura de dos documents JSON."
+ },
+ "jsonpathPlayground": {
+ "title": "Playground JSONPath",
+ "description": "Consulta JSON amb JSONPath o JMESPath i mira els resultats en directe."
+ },
+ "keycodeInspector": {
+ "title": "Inspector de keycode",
+ "description": "Prem una tecla per veure els seus codis d'esdeveniment JavaScript i modificadors."
+ },
+ "markdownTableGenerator": {
+ "title": "Generador de taules Markdown",
+ "description": "Crea taules Markdown i HTML en un editor de graella tipus full de càlcul."
+ },
+ "timezoneConverter": {
+ "title": "Convertidor de zona horària",
+ "description": "Converteix una hora entre moltes zones amb indicacions de canvi de dia i horari laboral."
+ },
+ "tokenCounter": {
+ "title": "Comptador de tokens LLM",
+ "description": "Compta tokens LLM, visualitza chunks i estima el cost de l'API."
+ },
+ "webhookTester": {
+ "title": "Provador de webhooks",
+ "description": "Captura i inspecciona les peticions de webhook entrants en temps real."
+ },
+ "websocketTester": {
+ "title": "Provador de WebSocket",
+ "description": "Connecta't a endpoints WebSocket i intercanvia missatges en temps real."
+ },
+ "whoisLookup": {
+ "title": "Consulta Whois",
+ "description": "Consulta les dades de registre de dominis i IP mitjançant RDAP."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Generador i verificador Bcrypt",
+ "subtitle": "Genera hash de contrasenyes amb bcrypt i verifica hashes — tot es queda al teu navegador",
+ "tabs": {
+ "hash": "Hash",
+ "verify": "Verifica"
+ },
+ "showPassword": "Mostra la contrasenya",
+ "hidePassword": "Amaga la contrasenya",
+ "copy": "Copia",
+ "copied": "Copiat",
+ "hash": {
+ "passwordLabel": "Contrasenya",
+ "passwordPlaceholder": "Introdueix una contrasenya per generar el hash…",
+ "costLabel": "Factor de cost",
+ "costHint": "Un cost més alt duplica la feina per pas. 10-12 és una opció habitual per a producció.",
+ "slowWarning": "Els factors de cost de 13 o més poden trigar diversos segons a calcular-se.",
+ "generate": "Genera el hash",
+ "generating": "Generant el hash…",
+ "resultLabel": "Hash bcrypt",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "Anatomia del hash",
+ "algorithm": "algoritme",
+ "cost": "cost",
+ "salt": "salt (22 caràcters)",
+ "hash": "hash (31 caràcters)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Contrasenya",
+ "passwordPlaceholder": "Introdueix la contrasenya per comprovar-la…",
+ "hashLabel": "Hash bcrypt",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Introdueix una contrasenya i un hash bcrypt per comparar-los.",
+ "invalidHash": "Això no sembla un hash bcrypt vàlid.",
+ "checking": "Comparant…",
+ "match": "Coincideix — la contrasenya produeix aquest hash.",
+ "noMatch": "No coincideix — la contrasenya no produeix aquest hash."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Calculadora Chmod",
+ "subtitle": "Converteix permisos de fitxer Unix entre caselles de selecció, octal i notació simbòlica",
+ "grid": {
+ "title": "Permisos",
+ "read": "Lectura",
+ "write": "Escriptura",
+ "execute": "Execució",
+ "owner": "Propietari",
+ "group": "Grup",
+ "others": "Altres"
+ },
+ "special": {
+ "title": "Bits especials",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "Octal",
+ "placeholder": "755 o 4755",
+ "invalid": "Introdueix 3 o 4 dígits octals (0-7)."
+ },
+ "symbolic": {
+ "label": "Simbòlic",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Introdueix 9 caràcters com rwxr-xr-x (s/S, t/T permesos)."
+ },
+ "output": {
+ "title": "Ordre",
+ "copy": "Copia",
+ "copied": "Copiat"
+ },
+ "explain": {
+ "line": "{cls} pot {perms}.",
+ "none": "{cls} no té permisos.",
+ "setuid": "Setuid: el fitxer s'executa amb els privilegis del seu propietari.",
+ "setgid": "Setgid: el fitxer s'executa amb els privilegis del seu grup (els fitxers nous d'un directori n'hereten el grup).",
+ "sticky": "Sticky: només el propietari d'un fitxer pot eliminar-lo o canviar-ne el nom dins d'aquest directori."
+ },
+ "presets": {
+ "title": "Predefinits habituals",
+ "octal": "Octal",
+ "permissions": "Permisos",
+ "description": "Ús típic",
+ "p644": "Fitxers normals: el propietari escriu, tothom llegeix.",
+ "p755": "Executables i directoris: tothom pot llegir i entrar-hi.",
+ "p600": "Fitxers privats: només el propietari llegeix i escriu.",
+ "p700": "Executables i directoris privats: només el propietari.",
+ "p664": "Fitxers on el grup pot escriure.",
+ "p775": "Directoris i executables on el grup pot escriure.",
+ "p400": "Només de lectura per al propietari (p. ex. claus privades SSH)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Captura de codi",
+ "subtitle": "Converteix codi en imatges boniques i compartibles",
+ "languageLabel": "Llenguatge",
+ "autoDetect": "Detecció automàtica",
+ "backgroundLabel": "Fons",
+ "paddingLabel": "Farciment",
+ "filenameLabel": "Nom del fitxer",
+ "filenamePlaceholder": "exemple.ts",
+ "windowLabel": "Punts de la finestra",
+ "themeLabel": "Tema de la finestra",
+ "themeDark": "Fosc",
+ "themeLight": "Clar",
+ "scaleLabel": "Escala d'exportació",
+ "copyImage": "Copia la imatge",
+ "copied": "Copiat",
+ "exportPng": "Exporta PNG",
+ "inputLabel": "Codi",
+ "detected": "Detectat: {language}",
+ "clear": "Netejar",
+ "placeholder": "Enganxa el teu codi aquí…",
+ "previewLabel": "Vista prèvia",
+ "emptyPreview": "La vista prèvia del teu codi apareixerà aquí",
+ "exportFailed": "No s'ha pogut exportar la imatge",
+ "copyUnsupported": "La còpia d'imatges no és compatible amb aquest navegador",
+ "copyFailed": "No s'ha pogut copiar la imatge",
+ "backgrounds": {
+ "ocean": "Oceà",
+ "sunset": "Posta de sol",
+ "candy": "Caramel",
+ "forest": "Bosc",
+ "midnight": "Mitjanit",
+ "gold": "Or",
+ "aurora": "Aurora",
+ "slate": "Pissarra",
+ "transparent": "Transparent"
+ }
+ },
+ "CssGenerators": {
+ "title": "Generadors CSS",
+ "subtitle": "Box-shadow, border-radius i clamp() fluid — copia el CSS.",
+ "preview": "Vista prèvia",
+ "output": "CSS",
+ "copy": "Copia",
+ "copied": "Copiat",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "clamp() fluid"
+ },
+ "boxShadow": {
+ "presets": "Predefinits",
+ "layer": "Capa",
+ "addLayer": "Afegeix una capa",
+ "removeLayer": "Elimina la capa",
+ "x": "X",
+ "y": "Y",
+ "blur": "Difuminat",
+ "spread": "Extensió",
+ "color": "Color",
+ "alpha": "Alfa",
+ "inset": "Interior",
+ "presetNames": {
+ "subtleCard": "Targeta subtil",
+ "elevated": "Elevat",
+ "hardOffset": "Desplaçament dur",
+ "neonGlow": "Resplendor neó",
+ "inner": "Interior",
+ "layeredSmooth": "Capes suaus"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Mode",
+ "modes": {
+ "simple": "Per cantonada",
+ "blob": "Bombolla orgànica"
+ },
+ "unit": "Unitat",
+ "linkCorners": "Enllaça les cantonades",
+ "randomize": "Aleatoritza",
+ "horizontal": "Radis horitzontals",
+ "vertical": "Radis verticals",
+ "corners": {
+ "tl": "Superior esquerra",
+ "tr": "Superior dreta",
+ "br": "Inferior dreta",
+ "bl": "Inferior esquerra"
+ }
+ },
+ "clamp": {
+ "minFont": "Font mínima (px)",
+ "maxFont": "Font màxima (px)",
+ "minViewport": "Viewport mínim (px)",
+ "maxViewport": "Viewport màxim (px)",
+ "rootFont": "Font arrel (px)",
+ "viewportWidth": "Viewport simulat",
+ "computedSize": "Mida calculada",
+ "slope": "Pendent",
+ "intercept": "Intercepció",
+ "previewText": "La ràpida guineu marró",
+ "invalid": "Introdueix límits de viewport vàlids i diferents."
+ }
+ },
+ "ExifViewer": {
+ "title": "Visualitzador i eliminador EXIF",
+ "subtitle": "Inspecciona les metadades de les fotos i elimina-les al teu navegador",
+ "dropTitle": "Deixa anar una imatge aquí",
+ "dropHint": "o fes clic per navegar — la teva imatge no surt mai del teu dispositiu",
+ "browse": "Tria una imatge",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Llegint metadades…",
+ "parseFailed": "No s'ha pogut llegir aquesta imatge",
+ "clear": "Elimina la imatge",
+ "gpsWarning": "Conté ubicació GPS",
+ "openMap": "Mostra a OpenStreetMap",
+ "noMetadata": "No s'han trobat metadades incrustades en aquesta imatge.",
+ "facts": {
+ "name": "Nom",
+ "type": "Tipus",
+ "dimensions": "Dimensions",
+ "size": "Mida"
+ },
+ "groups": {
+ "camera": "Càmera",
+ "exposure": "Exposició",
+ "timestamps": "Marques de temps",
+ "gps": "Ubicació GPS",
+ "software": "Programari i altres"
+ },
+ "gpsCard": {
+ "latitude": "Latitud",
+ "longitude": "Longitud",
+ "altitude": "Altitud"
+ },
+ "raw": {
+ "title": "Totes les metadades",
+ "copyJson": "Copia el JSON",
+ "copied": "Copiat"
+ },
+ "remove": {
+ "title": "Elimina les metadades",
+ "jpegDescription": "Elimina sense pèrdua EXIF, XMP, IPTC i comentaris sense recomprimir la imatge.",
+ "reencodeDescription": "Recodifica la imatge per eliminar les metadades incrustades. Això recomprimeix els píxels.",
+ "keepIcc": "Conserva el perfil de color (ICC)",
+ "stripButton": "Elimina les metadades",
+ "reencodeButton": "Recodifica i elimina",
+ "unsupported": "L'eliminació de metadades no és compatible amb aquest tipus de fitxer.",
+ "failed": "No s'han pogut eliminar les metadades",
+ "before": "Abans",
+ "after": "Després",
+ "download": "Descarrega la imatge neta"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Generador de favicon",
+ "subtitle": "Crea favicons i icones d'app a partir d'una imatge",
+ "dropTitle": "Deixa anar una imatge aquí",
+ "dropHint": "PNG, JPEG o SVG — fes clic per navegar",
+ "browse": "Tria una imatge",
+ "clear": "Elimina la imatge",
+ "backgroundLabel": "Fons",
+ "transparent": "Transparent",
+ "solidColor": "Sòlid",
+ "cornerLabel": "Forma de la cantonada",
+ "cornerNone": "Quadrat",
+ "cornerRounded": "Arrodonit",
+ "cornerCircle": "Cercle",
+ "maskPaddingLabel": "Farciment maskable",
+ "maskable": "maskable",
+ "previewLabel": "Vista prèvia",
+ "copy": "Copia",
+ "headSnippet": "Fragment del head HTML",
+ "downloadIco": "Descarrega favicon.ico",
+ "downloadZip": "Descarrega-ho tot (.zip)"
+ },
+ "JsonDiff": {
+ "title": "Diff JSON",
+ "subtitle": "Comparació semàntica i conscient de l'estructura de dos documents JSON — s'ignoren l'ordre de les claus i el format",
+ "options": {
+ "ignoreArrayOrder": "Ignora l'ordre dels arrays"
+ },
+ "loadBothSamples": "Carrega exemples",
+ "sample": "Exemple",
+ "format": "Formata",
+ "clear": "Netejar",
+ "formatError": "No es pot formatar: l'entrada no és un JSON vàlid",
+ "placeholder": "Enganxa o escriu JSON aquí…",
+ "panels": {
+ "left": "Original (esquerra)",
+ "right": "Modificat (dreta)",
+ "results": "Diferències"
+ },
+ "summary": {
+ "added": "{count} afegit(s)",
+ "removed": "{count} eliminat(s)",
+ "changed": "{count} canviat(s)"
+ },
+ "types": {
+ "added": "Afegit",
+ "removed": "Eliminat",
+ "changed": "Canviat"
+ },
+ "copyChanges": "Copia com a JSON",
+ "copied": "Copiat",
+ "empty": "Enganxa JSON als dos plafons per veure'n les diferències.",
+ "identical": "Els dos documents són semànticament idèntics."
+ },
+ "JsonpathPlayground": {
+ "title": "Playground JSONPath",
+ "subtitle": "Consulta documents JSON amb JSONPath o JMESPath i mira els resultats en directe",
+ "engineLabel": "Motor",
+ "queryLabel": "Consulta",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Referència ràpida",
+ "cheats": {
+ "jpAllTitles": "Tots els títols de llibres",
+ "jpAllAuthors": "Tots els autors, a qualsevol lloc del document",
+ "jpFirstBook": "Primer llibre",
+ "jpLastBook": "Últim llibre",
+ "jpSlice": "Els dos primers llibres (slice)",
+ "jpFilterPrice": "Llibres més barats que 40",
+ "jpFilterInStock": "Títols dels llibres en estoc",
+ "jpRecursive": "Tots els valors, recursivament",
+ "jmAllTitles": "Tots els títols de llibres",
+ "jmFirstAuthor": "Autor del primer llibre",
+ "jmLastBook": "Últim llibre",
+ "jmSlice": "Els dos primers llibres (slice)",
+ "jmFilterPrice": "Títols dels llibres més barats que 40",
+ "jmCount": "Compta els llibres en estoc",
+ "jmProjection": "Remodela cada llibre en un objecte nou",
+ "jmSort": "Títols ordenats per preu"
+ },
+ "panels": {
+ "input": "Entrada JSON",
+ "results": "Resultats"
+ },
+ "sample": "Exemple",
+ "format": "Formata",
+ "clear": "Netejar",
+ "formatError": "No es pot formatar: l'entrada no és un JSON vàlid",
+ "placeholder": "Enganxa o escriu JSON aquí…",
+ "outputPlaceholder": "Les coincidències apareixeran aquí…",
+ "matchCount": "{count, plural, one {# coincidència} other {# coincidències}}",
+ "copy": "Copia",
+ "copied": "Copiat"
+ },
+ "KeycodeInspector": {
+ "title": "Inspector de keycode",
+ "subtitle": "Prem qualsevol tecla per inspeccionar el seu esdeveniment de teclat JavaScript",
+ "capture": {
+ "prompt": "Prem qualsevol tecla…",
+ "ready": "Escoltant — prem qualsevol tecla del teu teclat",
+ "clickToFocus": "Fes clic aquí per enfocar i després prem qualsevol tecla"
+ },
+ "hero": {
+ "spaceKey": "(Espai)",
+ "keyLabel": "event.key",
+ "repeat": "repetició"
+ },
+ "copyJson": "Copia el JSON",
+ "copiedJson": "JSON de l'esdeveniment copiat al porta-retalls",
+ "deprecated": "obsolet",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Estàndard",
+ "left": "Esquerra",
+ "right": "Dreta",
+ "numpad": "Teclat numèric",
+ "unknown": "Desconegut"
+ },
+ "history": {
+ "title": "Historial",
+ "clear": "Neteja l'historial",
+ "empty": "Les tecles premudes apareixeran aquí."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Generador de taules Markdown",
+ "subtitle": "Edita una graella, enganxa des d'un full de càlcul i copia una taula Markdown o HTML ben alineada",
+ "addRow": "Afegeix una fila",
+ "addColumn": "Afegeix una columna",
+ "importLabel": "Importa",
+ "importPlaceholder": "Enganxa TSV (d'Excel / Google Sheets) o CSV aquí…",
+ "importButton": "Importa a la graella",
+ "pasteClipboard": "Enganxa des del porta-retalls",
+ "importEmpty": "Res per importar — enganxa primer algunes files",
+ "clipboardError": "No s'ha pogut llegir del porta-retalls",
+ "clear": "Netejar",
+ "panels": {
+ "editor": "Editor de taules",
+ "output": "Resultat"
+ },
+ "align": {
+ "left": "Alinea a l'esquerra",
+ "center": "Centra",
+ "right": "Alinea a la dreta"
+ },
+ "headerPlaceholder": "Capçalera",
+ "cellPlaceholder": "Cel·la",
+ "removeRow": "Elimina la fila",
+ "removeColumn": "Elimina la columna",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Copia",
+ "copied": "Copiat"
+ },
+ "TimezoneConverter": {
+ "title": "Convertidor de zona horària",
+ "subtitle": "Converteix un moment arreu del món per planificar reunions.",
+ "dateTimeLabel": "Data i hora",
+ "now": "Ara",
+ "sourceZoneLabel": "Zona horària d'origen",
+ "selectZone": "Selecciona una zona",
+ "searchZone": "Cerca zones horàries...",
+ "noZone": "No s'ha trobat cap zona horària.",
+ "scrubLabel": "Desplaça l'hora",
+ "reset": "Restableix",
+ "targetsLabel": "Zones de destinació",
+ "addZone": "Afegeix una zona",
+ "remove": "Elimina",
+ "localBadge": "Local",
+ "invalidDate": "Introdueix una data i hora vàlides.",
+ "bands": {
+ "work": "Horari laboral (09:00–18:00)",
+ "edge": "Hores límit (matí / vespre)",
+ "night": "Nit"
+ }
+ },
+ "TokenCounter": {
+ "title": "Comptador de tokens LLM",
+ "subtitle": "Compta tokens, visualitza chunks i estima el cost de l'API.",
+ "inputLabel": "Text",
+ "sample": "Exemple",
+ "clear": "Netejar",
+ "placeholder": "Enganxa text, codi o JSON per comptar tokens...",
+ "encoding": "Codificació",
+ "loading": "Carregant el tokenitzador...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / sèrie o)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Tokens",
+ "characters": "Caràcters",
+ "words": "Paraules",
+ "ratio": "Tokens/paraula",
+ "ratioSub": "aprox."
+ },
+ "claudeRow": "Claude (aproximat)",
+ "estimate": "estimació",
+ "viz": {
+ "title": "Visualització de tokens",
+ "toggle": "Mostra",
+ "hint": "Activa-ho per acolorir cada token com un chunk.",
+ "empty": "Introdueix text per visualitzar els tokens.",
+ "truncated": "Mostrant els primers {limit} de {total} tokens."
+ },
+ "cost": {
+ "title": "Calculadora de cost",
+ "disclaimer": "valors per defecte editables — verifica els preus actuals",
+ "model": "Model",
+ "expectedOutput": "Tokens de sortida esperats",
+ "customInput": "Entrada $/MTok",
+ "customOutput": "Sortida $/MTok",
+ "inputCost": "Cost d'entrada",
+ "outputCost": "Cost de sortida",
+ "total": "Total",
+ "per1000": "Per 1000 crides"
+ }
+ },
+ "WebhookTester": {
+ "title": "Provador de webhooks",
+ "subtitle": "Crea una URL temporal i inspecciona les peticions de webhook entrants en directe.",
+ "intro": {
+ "title": "Crea una URL de webhook per començar a capturar peticions",
+ "hint": "Rebràs una URL impossible d'endevinar que registra cada petició entrant. Caduca al cap de 24 hores."
+ },
+ "createBtn": "Crea una URL de webhook",
+ "creating": "Creant…",
+ "urlLabel": "La teva URL de webhook",
+ "expiresIn": "Caduca en {countdown}",
+ "copyUrlBtn": "Copia la URL",
+ "copied": "Copiat",
+ "clearBtn": "Netejar",
+ "newBinBtn": "URL nova",
+ "requestsTitle": "Peticions",
+ "selectPrompt": "Selecciona una petició per inspeccionar-ne els detalls.",
+ "expiredNotice": "Aquesta URL de webhook ha caducat. Crea'n una de nova per continuar provant.",
+ "base64Badge": "base64",
+ "truncatedBadge": "truncat",
+ "empty": {
+ "title": "Esperant peticions…",
+ "hint": "Envia una petició a la teva URL i apareixerà aquí a l'instant. Prova això:"
+ },
+ "detail": {
+ "receivedAt": "Rebut",
+ "source": "IP d'origen",
+ "contentType": "Content-Type",
+ "query": "Paràmetres de query",
+ "headers": "Capçaleres",
+ "body": "Cos",
+ "copyBody": "Copia el cos",
+ "emptyBody": "Sense cos"
+ },
+ "errors": {
+ "createFailed": "No s'ha pogut crear la URL de webhook. Torna-ho a provar.",
+ "clearFailed": "No s'han pogut netejar les peticions. Torna-ho a provar."
+ }
+ },
+ "WebsocketTester": {
+ "title": "Provador de WebSocket",
+ "subtitle": "Connecta't a un endpoint WebSocket i envia, rep i inspecciona missatges en directe",
+ "urlLabel": "URL de l'endpoint",
+ "protocolsLabel": "Subprotocols",
+ "protocolsPlaceholder": "opcional, separats per comes",
+ "connect": "Connecta",
+ "disconnect": "Desconnecta",
+ "status": {
+ "disconnected": "Desconnectat",
+ "connecting": "Connectant",
+ "connected": "Connectat",
+ "error": "Error"
+ },
+ "messages": {
+ "connecting": "Connectant a {url}…",
+ "connected": "Connexió establerta",
+ "connectedWithProtocol": "Connexió establerta (protocol: {protocol})",
+ "invalidUrl": "URL no vàlida — ha de començar amb ws:// o wss://",
+ "connectFailed": "No s'ha pogut obrir la connexió",
+ "socketError": "S'ha produït un error de connexió",
+ "notConnected": "No connectat — connecta't primer",
+ "binaryMessage": "S'ha rebut un missatge binari"
+ },
+ "log": {
+ "title": "Registre de missatges",
+ "empty": "Encara no hi ha missatges. Connecta't i comença a enviar.",
+ "copyAll": "Copia-ho tot",
+ "download": "Descarrega com a .txt",
+ "clear": "Neteja el registre",
+ "prettyPrint": "Formata el JSON",
+ "showRaw": "Mostra el text en brut"
+ },
+ "composer": {
+ "placeholder": "Escriu un missatge per enviar…",
+ "send": "Envia",
+ "formatJson": "Formata com a JSON",
+ "invalidJson": "El missatge no és un JSON vàlid",
+ "hint": "Cmd/Ctrl+Enter per enviar"
+ },
+ "ping": {
+ "label": "Auto-ping",
+ "seconds": "s",
+ "intervalTitle": "Interval de ping en segons",
+ "payloadPlaceholder": "payload del ping"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Consulta Whois",
+ "subtitle": "Consulta les dades de registre de qualsevol domini o adreça IP mitjançant RDAP",
+ "placeholder": "example.com o 8.8.8.8",
+ "lookupBtn": "Consulta",
+ "looking": "Consultant…",
+ "refreshBtn": "Actualitza",
+ "tryLabel": "Prova:",
+ "copy": "Copia",
+ "copied": "Copiat",
+ "notAvailable": "No disponible",
+ "summary": {
+ "registrar": "Registrador",
+ "network": "Xarxa"
+ },
+ "dates": {
+ "created": "Creat",
+ "updated": "Actualitzat",
+ "expires": "Caduca"
+ },
+ "sections": {
+ "status": "Estat",
+ "nameservers": "Servidors de noms",
+ "contacts": "Contactes",
+ "rawRdap": "RDAP en brut"
+ },
+ "contacts": {
+ "roles": "Rols",
+ "name": "Nom",
+ "organization": "Organització"
+ },
+ "errors": {
+ "invalidTarget": "Introdueix un nom de domini o adreça IP vàlids",
+ "lookupFailed": "La consulta ha fallat (estat {status})"
+ }
}
}
diff --git a/apps/web/messages/cs.json b/apps/web/messages/cs.json
index 176e845c..3080645c 100644
--- a/apps/web/messages/cs.json
+++ b/apps/web/messages/cs.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Kalkulačka chmod",
+ "codeScreenshot": "Snímek kódu",
+ "cssGenerators": "CSS generátory",
+ "exifViewer": "EXIF prohlížeč",
+ "faviconGenerator": "Generátor favicon",
+ "jsonDiff": "JSON diff",
+ "jsonpathPlayground": "JSONPath hřiště",
+ "keycodeInspector": "Inspektor kláves",
+ "markdownTableGenerator": "Markdown tabulka",
+ "timezoneConverter": "Převodník časových pásem",
+ "tokenCounter": "Počítadlo tokenů",
+ "webhookTester": "Tester webhooků",
+ "websocketTester": "Tester WebSocket",
+ "whoisLookup": "Whois vyhledávání"
},
"Help": {
"title": "Nápověda a dokumentace",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Bcrypt generátor a ověřovač",
+ "description": "Hashujte hesla pomocí bcrypt a ověřujte stávající hashe."
+ },
+ "chmodCalculator": {
+ "title": "Kalkulačka chmod",
+ "description": "Převádějte unixová oprávnění mezi osmičkovým, symbolickým zápisem a zaškrtávacími políčky."
+ },
+ "codeScreenshot": {
+ "title": "Snímek kódu",
+ "description": "Proměňte zdrojový kód v krásné sdílitelné obrázky."
+ },
+ "cssGenerators": {
+ "title": "CSS generátory",
+ "description": "Generátory box-shadow, border-radius a plynulého clamp()."
+ },
+ "exifViewer": {
+ "title": "EXIF prohlížeč a odstraňovač",
+ "description": "Prohlížejte a odstraňujte EXIF metadata fotek lokálně."
+ },
+ "faviconGenerator": {
+ "title": "Generátor favicon",
+ "description": "Generujte favicony, ikony aplikací a úryvky manifestu."
+ },
+ "jsonDiff": {
+ "title": "JSON diff",
+ "description": "Sémantické porovnání dvou JSON dokumentů zohledňující strukturu."
+ },
+ "jsonpathPlayground": {
+ "title": "JSONPath hřiště",
+ "description": "Dotazujte se na JSON pomocí JSONPath nebo JMESPath a sledujte živé výsledky."
+ },
+ "keycodeInspector": {
+ "title": "Inspektor kláves",
+ "description": "Stiskněte klávesu a zobrazte její JavaScriptové kódy událostí a modifikátory."
+ },
+ "markdownTableGenerator": {
+ "title": "Generátor Markdown tabulek",
+ "description": "Vytvářejte Markdown a HTML tabulky v tabulkovém editoru připomínajícím sešit."
+ },
+ "timezoneConverter": {
+ "title": "Převodník časových pásem",
+ "description": "Převádějte čas mezi mnoha pásmy s posunem dne a náznaky pracovní doby."
+ },
+ "tokenCounter": {
+ "title": "Počítadlo LLM tokenů",
+ "description": "Počítejte LLM tokeny, vizualizujte bloky a odhadněte náklady na API."
+ },
+ "webhookTester": {
+ "title": "Tester webhooků",
+ "description": "Zachytávejte a prohlížejte příchozí webhook požadavky v reálném čase."
+ },
+ "websocketTester": {
+ "title": "Tester WebSocket",
+ "description": "Připojte se k WebSocket koncovým bodům a vyměňujte zprávy v reálném čase."
+ },
+ "whoisLookup": {
+ "title": "Whois vyhledávání",
+ "description": "Vyhledejte registrační údaje domény a IP adresy přes RDAP."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Bcrypt generátor a ověřovač",
+ "subtitle": "Hashujte hesla pomocí bcrypt a ověřujte hashe — vše zůstává ve vašem prohlížeči",
+ "tabs": {
+ "hash": "Hash",
+ "verify": "Ověřit"
+ },
+ "showPassword": "Zobrazit heslo",
+ "hidePassword": "Skrýt heslo",
+ "copy": "Kopírovat",
+ "copied": "Zkopírováno",
+ "hash": {
+ "passwordLabel": "Heslo",
+ "passwordPlaceholder": "Zadejte heslo k zahashování…",
+ "costLabel": "Faktor náročnosti",
+ "costHint": "Vyšší náročnost zdvojnásobuje práci na každý krok. 10–12 je běžná volba pro produkci.",
+ "slowWarning": "Faktory náročnosti 13 a vyšší mohou výpočet trvat několik sekund.",
+ "generate": "Vygenerovat hash",
+ "generating": "Hashuji…",
+ "resultLabel": "Bcrypt hash",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "Anatomie hashe",
+ "algorithm": "algoritmus",
+ "cost": "náročnost",
+ "salt": "sůl (22 znaků)",
+ "hash": "hash (31 znaků)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Heslo",
+ "passwordPlaceholder": "Zadejte heslo k ověření…",
+ "hashLabel": "Bcrypt hash",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Zadejte heslo a bcrypt hash k jejich porovnání.",
+ "invalidHash": "Toto nevypadá jako platný bcrypt hash.",
+ "checking": "Porovnávám…",
+ "match": "Shoda — heslo vytváří tento hash.",
+ "noMatch": "Neshoda — heslo nevytváří tento hash."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Kalkulačka chmod",
+ "subtitle": "Převádějte unixová oprávnění souborů mezi zaškrtávacími políčky, osmičkovým a symbolickým zápisem",
+ "grid": {
+ "title": "Oprávnění",
+ "read": "Čtení",
+ "write": "Zápis",
+ "execute": "Spouštění",
+ "owner": "Vlastník",
+ "group": "Skupina",
+ "others": "Ostatní"
+ },
+ "special": {
+ "title": "Speciální bity",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "Osmičkově",
+ "placeholder": "755 nebo 4755",
+ "invalid": "Zadejte 3 nebo 4 osmičkové číslice (0–7)."
+ },
+ "symbolic": {
+ "label": "Symbolicky",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Zadejte 9 znaků jako rwxr-xr-x (povoleno s/S, t/T)."
+ },
+ "output": {
+ "title": "Příkaz",
+ "copy": "Kopírovat",
+ "copied": "Zkopírováno"
+ },
+ "explain": {
+ "line": "{cls} může {perms}.",
+ "none": "{cls} nemá žádná oprávnění.",
+ "setuid": "Setuid: soubor běží s právy svého vlastníka.",
+ "setgid": "Setgid: soubor běží s právy své skupiny (nové soubory v adresáři dědí jeho skupinu).",
+ "sticky": "Sticky: soubor v tomto adresáři může smazat nebo přejmenovat pouze jeho vlastník."
+ },
+ "presets": {
+ "title": "Běžné předvolby",
+ "octal": "Osmičkově",
+ "permissions": "Oprávnění",
+ "description": "Typické použití",
+ "p644": "Běžné soubory: vlastník zapisuje, všichni čtou.",
+ "p755": "Spustitelné soubory a adresáře: všichni mohou číst a vstoupit.",
+ "p600": "Soukromé soubory: čte a zapisuje pouze vlastník.",
+ "p700": "Soukromé spustitelné soubory a adresáře: pouze vlastník.",
+ "p664": "Soubory zapisovatelné skupinou.",
+ "p775": "Adresáře a spustitelné soubory zapisovatelné skupinou.",
+ "p400": "Pouze pro čtení vlastníkem (např. soukromé SSH klíče)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Snímek kódu",
+ "subtitle": "Proměňte kód v krásné sdílitelné obrázky",
+ "languageLabel": "Jazyk",
+ "autoDetect": "Automatická detekce",
+ "backgroundLabel": "Pozadí",
+ "paddingLabel": "Odsazení",
+ "filenameLabel": "Název souboru",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "Tečky okna",
+ "themeLabel": "Motiv okna",
+ "themeDark": "Tmavý",
+ "themeLight": "Světlý",
+ "scaleLabel": "Měřítko exportu",
+ "copyImage": "Kopírovat obrázek",
+ "copied": "Zkopírováno",
+ "exportPng": "Exportovat PNG",
+ "inputLabel": "Kód",
+ "detected": "Detekováno: {language}",
+ "clear": "Vymazat",
+ "placeholder": "Vložte sem svůj kód…",
+ "previewLabel": "Náhled",
+ "emptyPreview": "Náhled vašeho kódu se zobrazí zde",
+ "exportFailed": "Export obrázku se nezdařil",
+ "copyUnsupported": "Kopírování obrázků není v tomto prohlížeči podporováno",
+ "copyFailed": "Kopírování obrázku se nezdařilo",
+ "backgrounds": {
+ "ocean": "Oceán",
+ "sunset": "Západ slunce",
+ "candy": "Bonbon",
+ "forest": "Les",
+ "midnight": "Půlnoc",
+ "gold": "Zlatá",
+ "aurora": "Polární záře",
+ "slate": "Břidlice",
+ "transparent": "Průhledné"
+ }
+ },
+ "CssGenerators": {
+ "title": "CSS generátory",
+ "subtitle": "Box-shadow, border-radius a plynulý clamp() — zkopírujte CSS.",
+ "preview": "Náhled",
+ "output": "CSS",
+ "copy": "Kopírovat",
+ "copied": "Zkopírováno",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "Plynulý clamp()"
+ },
+ "boxShadow": {
+ "presets": "Předvolby",
+ "layer": "Vrstva",
+ "addLayer": "Přidat vrstvu",
+ "removeLayer": "Odebrat vrstvu",
+ "x": "X",
+ "y": "Y",
+ "blur": "Rozmazání",
+ "spread": "Rozšíření",
+ "color": "Barva",
+ "alpha": "Alfa",
+ "inset": "Vnitřní",
+ "presetNames": {
+ "subtleCard": "Jemná karta",
+ "elevated": "Vyvýšená",
+ "hardOffset": "Ostrý posun",
+ "neonGlow": "Neonová záře",
+ "inner": "Vnitřní",
+ "layeredSmooth": "Vrstvená hladká"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Režim",
+ "modes": {
+ "simple": "Po rozích",
+ "blob": "Organická skvrna"
+ },
+ "unit": "Jednotka",
+ "linkCorners": "Propojit rohy",
+ "randomize": "Náhodně",
+ "horizontal": "Vodorovné poloměry",
+ "vertical": "Svislé poloměry",
+ "corners": {
+ "tl": "Vlevo nahoře",
+ "tr": "Vpravo nahoře",
+ "br": "Vpravo dole",
+ "bl": "Vlevo dole"
+ }
+ },
+ "clamp": {
+ "minFont": "Min. písmo (px)",
+ "maxFont": "Max. písmo (px)",
+ "minViewport": "Min. viewport (px)",
+ "maxViewport": "Max. viewport (px)",
+ "rootFont": "Kořenové písmo (px)",
+ "viewportWidth": "Simulovaný viewport",
+ "computedSize": "Vypočtená velikost",
+ "slope": "Sklon",
+ "intercept": "Průsečík",
+ "previewText": "Příliš žluťoučký kůň úpěl",
+ "invalid": "Zadejte platné, odlišné hranice viewportu."
+ }
+ },
+ "ExifViewer": {
+ "title": "EXIF prohlížeč a odstraňovač",
+ "subtitle": "Prohlížejte metadata fotek a odstraňte je ve svém prohlížeči",
+ "dropTitle": "Sem přetáhněte obrázek",
+ "dropHint": "nebo klikněte pro procházení — váš obrázek nikdy neopustí vaše zařízení",
+ "browse": "Vybrat obrázek",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Čtu metadata…",
+ "parseFailed": "Čtení tohoto obrázku se nezdařilo",
+ "clear": "Odebrat obrázek",
+ "gpsWarning": "Obsahuje GPS polohu",
+ "openMap": "Zobrazit na OpenStreetMap",
+ "noMetadata": "V tomto obrázku nebyla nalezena žádná vložená metadata.",
+ "facts": {
+ "name": "Název",
+ "type": "Typ",
+ "dimensions": "Rozměry",
+ "size": "Velikost"
+ },
+ "groups": {
+ "camera": "Fotoaparát",
+ "exposure": "Expozice",
+ "timestamps": "Časové značky",
+ "gps": "GPS poloha",
+ "software": "Software a ostatní"
+ },
+ "gpsCard": {
+ "latitude": "Zeměpisná šířka",
+ "longitude": "Zeměpisná délka",
+ "altitude": "Nadmořská výška"
+ },
+ "raw": {
+ "title": "Všechna metadata",
+ "copyJson": "Kopírovat JSON",
+ "copied": "Zkopírováno"
+ },
+ "remove": {
+ "title": "Odstranit metadata",
+ "jpegDescription": "Bezeztrátově odstraňte EXIF, XMP, IPTC a komentáře bez opětovné komprese obrázku.",
+ "reencodeDescription": "Znovu zakódujte obrázek a odstraňte vložená metadata. Tímto se pixely znovu komprimují.",
+ "keepIcc": "Zachovat barevný profil (ICC)",
+ "stripButton": "Odstranit metadata",
+ "reencodeButton": "Znovu zakódovat a odstranit",
+ "unsupported": "Odstranění metadat není pro tento typ souboru podporováno.",
+ "failed": "Odstranění metadat se nezdařilo",
+ "before": "Před",
+ "after": "Po",
+ "download": "Stáhnout vyčištěný obrázek"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Generátor favicon",
+ "subtitle": "Vytvořte favicony a ikony aplikací z jednoho obrázku",
+ "dropTitle": "Sem přetáhněte obrázek",
+ "dropHint": "PNG, JPEG nebo SVG — klikněte pro procházení",
+ "browse": "Vybrat obrázek",
+ "clear": "Odebrat obrázek",
+ "backgroundLabel": "Pozadí",
+ "transparent": "Průhledné",
+ "solidColor": "Plná barva",
+ "cornerLabel": "Tvar rohů",
+ "cornerNone": "Hranatý",
+ "cornerRounded": "Zaoblený",
+ "cornerCircle": "Kruh",
+ "maskPaddingLabel": "Odsazení pro maskování",
+ "maskable": "maskable",
+ "previewLabel": "Náhled",
+ "copy": "Kopírovat",
+ "headSnippet": "Úryvek HTML head",
+ "downloadIco": "Stáhnout favicon.ico",
+ "downloadZip": "Stáhnout vše (.zip)"
+ },
+ "JsonDiff": {
+ "title": "JSON diff",
+ "subtitle": "Sémantické porovnání dvou JSON dokumentů zohledňující strukturu — pořadí klíčů a formátování se ignoruje",
+ "options": {
+ "ignoreArrayOrder": "Ignorovat pořadí polí"
+ },
+ "loadBothSamples": "Načíst ukázky",
+ "sample": "Ukázka",
+ "format": "Formátovat",
+ "clear": "Vymazat",
+ "formatError": "Nelze formátovat: vstup není platný JSON",
+ "placeholder": "Vložte nebo napište JSON sem…",
+ "panels": {
+ "left": "Originál (vlevo)",
+ "right": "Upravený (vpravo)",
+ "results": "Rozdíly"
+ },
+ "summary": {
+ "added": "{count} přidáno",
+ "removed": "{count} odebráno",
+ "changed": "{count} změněno"
+ },
+ "types": {
+ "added": "Přidáno",
+ "removed": "Odebráno",
+ "changed": "Změněno"
+ },
+ "copyChanges": "Kopírovat jako JSON",
+ "copied": "Zkopírováno",
+ "empty": "Vložte JSON do obou panelů, abyste viděli jejich rozdíly.",
+ "identical": "Oba dokumenty jsou sémanticky totožné."
+ },
+ "JsonpathPlayground": {
+ "title": "JSONPath hřiště",
+ "subtitle": "Dotazujte se na JSON dokumenty pomocí JSONPath nebo JMESPath a sledujte živé výsledky",
+ "engineLabel": "Engine",
+ "queryLabel": "Dotaz",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Tahák",
+ "cheats": {
+ "jpAllTitles": "Všechny názvy knih",
+ "jpAllAuthors": "Každý autor, kdekoli v dokumentu",
+ "jpFirstBook": "První kniha",
+ "jpLastBook": "Poslední kniha",
+ "jpSlice": "První dvě knihy (výřez)",
+ "jpFilterPrice": "Knihy levnější než 40",
+ "jpFilterInStock": "Názvy skladem dostupných knih",
+ "jpRecursive": "Všechny hodnoty, rekurzivně",
+ "jmAllTitles": "Všechny názvy knih",
+ "jmFirstAuthor": "Autor první knihy",
+ "jmLastBook": "Poslední kniha",
+ "jmSlice": "První dvě knihy (výřez)",
+ "jmFilterPrice": "Názvy knih levnějších než 40",
+ "jmCount": "Počet skladem dostupných knih",
+ "jmProjection": "Přeformujte každou knihu do nového objektu",
+ "jmSort": "Názvy seřazené podle ceny"
+ },
+ "panels": {
+ "input": "JSON vstup",
+ "results": "Výsledky"
+ },
+ "sample": "Ukázka",
+ "format": "Formátovat",
+ "clear": "Vymazat",
+ "formatError": "Nelze formátovat: vstup není platný JSON",
+ "placeholder": "Vložte nebo napište JSON sem…",
+ "outputPlaceholder": "Shody se zobrazí zde…",
+ "matchCount": "{count, plural, one {# shoda} few {# shody} many {# shody} other {# shod}}",
+ "copy": "Kopírovat",
+ "copied": "Zkopírováno"
+ },
+ "KeycodeInspector": {
+ "title": "Inspektor kláves",
+ "subtitle": "Stiskněte libovolnou klávesu a prozkoumejte její JavaScriptovou klávesovou událost",
+ "capture": {
+ "prompt": "Stiskněte libovolnou klávesu…",
+ "ready": "Naslouchám — stiskněte libovolnou klávesu na klávesnici",
+ "clickToFocus": "Klikněte sem pro zaměření, poté stiskněte libovolnou klávesu"
+ },
+ "hero": {
+ "spaceKey": "(Mezerník)",
+ "keyLabel": "event.key",
+ "repeat": "opakování"
+ },
+ "copyJson": "Kopírovat JSON",
+ "copiedJson": "JSON události zkopírován do schránky",
+ "deprecated": "zastaralé",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Standardní",
+ "left": "Levá",
+ "right": "Pravá",
+ "numpad": "Numerická klávesnice",
+ "unknown": "Neznámé"
+ },
+ "history": {
+ "title": "Historie",
+ "clear": "Vymazat historii",
+ "empty": "Stisknuté klávesy se zobrazí zde."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Generátor Markdown tabulek",
+ "subtitle": "Upravujte mřížku, vkládejte ze sešitu a kopírujte pěkně zarovnanou Markdown nebo HTML tabulku",
+ "addRow": "Přidat řádek",
+ "addColumn": "Přidat sloupec",
+ "importLabel": "Import",
+ "importPlaceholder": "Vložte sem TSV (z Excelu / Tabulek Google) nebo CSV…",
+ "importButton": "Importovat do mřížky",
+ "pasteClipboard": "Vložit ze schránky",
+ "importEmpty": "Není co importovat — nejprve vložte nějaké řádky",
+ "clipboardError": "Nepodařilo se číst ze schránky",
+ "clear": "Vymazat",
+ "panels": {
+ "editor": "Editor tabulky",
+ "output": "Výstup"
+ },
+ "align": {
+ "left": "Zarovnat vlevo",
+ "center": "Zarovnat na střed",
+ "right": "Zarovnat vpravo"
+ },
+ "headerPlaceholder": "Záhlaví",
+ "cellPlaceholder": "Buňka",
+ "removeRow": "Odebrat řádek",
+ "removeColumn": "Odebrat sloupec",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Kopírovat",
+ "copied": "Zkopírováno"
+ },
+ "TimezoneConverter": {
+ "title": "Převodník časových pásem",
+ "subtitle": "Převeďte jeden okamžik po celém světě pro plánování schůzek.",
+ "dateTimeLabel": "Datum a čas",
+ "now": "Nyní",
+ "sourceZoneLabel": "Zdrojové časové pásmo",
+ "selectZone": "Vyberte pásmo",
+ "searchZone": "Hledat časová pásma...",
+ "noZone": "Žádné časové pásmo nenalezeno.",
+ "scrubLabel": "Posunout čas",
+ "reset": "Obnovit",
+ "targetsLabel": "Cílová pásma",
+ "addZone": "Přidat pásmo",
+ "remove": "Odebrat",
+ "localBadge": "Místní",
+ "invalidDate": "Zadejte platné datum a čas.",
+ "bands": {
+ "work": "Pracovní doba (09:00–18:00)",
+ "edge": "Okrajové hodiny (ráno / večer)",
+ "night": "Noc"
+ }
+ },
+ "TokenCounter": {
+ "title": "Počítadlo LLM tokenů",
+ "subtitle": "Počítejte tokeny, vizualizujte bloky a odhadněte náklady na API.",
+ "inputLabel": "Text",
+ "sample": "Ukázka",
+ "clear": "Vymazat",
+ "placeholder": "Vložte text, kód nebo JSON pro počítání tokenů...",
+ "encoding": "Kódování",
+ "loading": "Načítám tokenizér...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / řada o)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Tokeny",
+ "characters": "Znaky",
+ "words": "Slova",
+ "ratio": "Tokeny/slovo",
+ "ratioSub": "přibližně"
+ },
+ "claudeRow": "Claude (přibližně)",
+ "estimate": "odhad",
+ "viz": {
+ "title": "Vizualizace tokenů",
+ "toggle": "Zobrazit",
+ "hint": "Přepněte pro obarvení každého tokenu jako bloku.",
+ "empty": "Zadejte text pro vizualizaci tokenů.",
+ "truncated": "Zobrazuji prvních {limit} z {total} tokenů."
+ },
+ "cost": {
+ "title": "Kalkulačka nákladů",
+ "disclaimer": "upravitelné výchozí hodnoty — ověřte si aktuální ceny",
+ "model": "Model",
+ "expectedOutput": "Očekávané výstupní tokeny",
+ "customInput": "Vstup $/MTok",
+ "customOutput": "Výstup $/MTok",
+ "inputCost": "Náklady na vstup",
+ "outputCost": "Náklady na výstup",
+ "total": "Celkem",
+ "per1000": "Na 1000 volání"
+ }
+ },
+ "WebhookTester": {
+ "title": "Tester webhooků",
+ "subtitle": "Vytvořte dočasnou URL a živě prohlížejte příchozí webhook požadavky.",
+ "intro": {
+ "title": "Vytvořte webhook URL pro zahájení zachytávání požadavků",
+ "hint": "Získáte neuhodnutelnou URL, která zaznamenává každý příchozí požadavek. Vyprší po 24 hodinách."
+ },
+ "createBtn": "Vytvořit webhook URL",
+ "creating": "Vytvářím…",
+ "urlLabel": "Vaše webhook URL",
+ "expiresIn": "Vyprší za {countdown}",
+ "copyUrlBtn": "Kopírovat URL",
+ "copied": "Zkopírováno",
+ "clearBtn": "Vymazat",
+ "newBinBtn": "Nová URL",
+ "requestsTitle": "Požadavky",
+ "selectPrompt": "Vyberte požadavek pro zobrazení jeho detailů.",
+ "expiredNotice": "Tato webhook URL vypršela. Vytvořte novou pro pokračování v testování.",
+ "base64Badge": "base64",
+ "truncatedBadge": "zkráceno",
+ "empty": {
+ "title": "Čekám na požadavky…",
+ "hint": "Odešlete požadavek na svou URL a okamžitě se zde objeví. Zkuste toto:"
+ },
+ "detail": {
+ "receivedAt": "Přijato",
+ "source": "Zdrojová IP",
+ "contentType": "Content-Type",
+ "query": "Parametry dotazu",
+ "headers": "Hlavičky",
+ "body": "Tělo",
+ "copyBody": "Kopírovat tělo",
+ "emptyBody": "Bez těla"
+ },
+ "errors": {
+ "createFailed": "Vytvoření webhook URL se nezdařilo. Zkuste to prosím znovu.",
+ "clearFailed": "Vymazání požadavků se nezdařilo. Zkuste to prosím znovu."
+ }
+ },
+ "WebsocketTester": {
+ "title": "Tester WebSocket",
+ "subtitle": "Připojte se k WebSocket koncovému bodu a živě odesílejte, přijímejte a prohlížejte zprávy",
+ "urlLabel": "URL koncového bodu",
+ "protocolsLabel": "Subprotokoly",
+ "protocolsPlaceholder": "volitelné, oddělené čárkou",
+ "connect": "Připojit",
+ "disconnect": "Odpojit",
+ "status": {
+ "disconnected": "Odpojeno",
+ "connecting": "Připojuji",
+ "connected": "Připojeno",
+ "error": "Chyba"
+ },
+ "messages": {
+ "connecting": "Připojuji k {url}…",
+ "connected": "Spojení navázáno",
+ "connectedWithProtocol": "Spojení navázáno (protokol: {protocol})",
+ "invalidUrl": "Neplatná URL — musí začínat ws:// nebo wss://",
+ "connectFailed": "Otevření spojení se nezdařilo",
+ "socketError": "Došlo k chybě spojení",
+ "notConnected": "Nepřipojeno — nejprve se připojte",
+ "binaryMessage": "Přijata binární zpráva"
+ },
+ "log": {
+ "title": "Protokol zpráv",
+ "empty": "Zatím žádné zprávy. Připojte se a začněte odesílat.",
+ "copyAll": "Kopírovat vše",
+ "download": "Stáhnout jako .txt",
+ "clear": "Vymazat protokol",
+ "prettyPrint": "Zformátovat JSON",
+ "showRaw": "Zobrazit surový text"
+ },
+ "composer": {
+ "placeholder": "Napište zprávu k odeslání…",
+ "send": "Odeslat",
+ "formatJson": "Formátovat jako JSON",
+ "invalidJson": "Zpráva není platný JSON",
+ "hint": "Cmd/Ctrl+Enter pro odeslání"
+ },
+ "ping": {
+ "label": "Automatický ping",
+ "seconds": "s",
+ "intervalTitle": "Interval pingu v sekundách",
+ "payloadPlaceholder": "obsah pingu"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Whois vyhledávání",
+ "subtitle": "Vyhledejte registrační údaje libovolné domény nebo IP adresy přes RDAP",
+ "placeholder": "example.com nebo 8.8.8.8",
+ "lookupBtn": "Vyhledat",
+ "looking": "Vyhledávám…",
+ "refreshBtn": "Obnovit",
+ "tryLabel": "Zkuste:",
+ "copy": "Kopírovat",
+ "copied": "Zkopírováno",
+ "notAvailable": "Není k dispozici",
+ "summary": {
+ "registrar": "Registrátor",
+ "network": "Síť"
+ },
+ "dates": {
+ "created": "Vytvořeno",
+ "updated": "Aktualizováno",
+ "expires": "Vyprší"
+ },
+ "sections": {
+ "status": "Stav",
+ "nameservers": "Jmenné servery",
+ "contacts": "Kontakty",
+ "rawRdap": "Surové RDAP"
+ },
+ "contacts": {
+ "roles": "Role",
+ "name": "Jméno",
+ "organization": "Organizace"
+ },
+ "errors": {
+ "invalidTarget": "Zadejte platný název domény nebo IP adresu",
+ "lookupFailed": "Vyhledávání se nezdařilo (stav {status})"
+ }
}
}
diff --git a/apps/web/messages/da.json b/apps/web/messages/da.json
index 94f1c534..6cd654ef 100644
--- a/apps/web/messages/da.json
+++ b/apps/web/messages/da.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Chmod-beregner",
+ "codeScreenshot": "Kodeskærmbillede",
+ "cssGenerators": "CSS-generatorer",
+ "exifViewer": "EXIF-fremviser",
+ "faviconGenerator": "Favicon-generator",
+ "jsonDiff": "JSON-diff",
+ "jsonpathPlayground": "JSONPath-legeplads",
+ "keycodeInspector": "Tastekode-inspektør",
+ "markdownTableGenerator": "Markdown-tabel",
+ "timezoneConverter": "Tidszonekonverter",
+ "tokenCounter": "Token-tæller",
+ "webhookTester": "Webhook-tester",
+ "websocketTester": "WebSocket-tester",
+ "whoisLookup": "Whois-opslag"
},
"Help": {
"title": "Hjælp og dokumentation",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Bcrypt-generator og -kontrol",
+ "description": "Hash adgangskoder med bcrypt, og verificér eksisterende hashes."
+ },
+ "chmodCalculator": {
+ "title": "Chmod-beregner",
+ "description": "Konvertér Unix-tilladelser mellem oktal, symbolsk notation og afkrydsningsfelter."
+ },
+ "codeScreenshot": {
+ "title": "Kodeskærmbillede",
+ "description": "Forvandl kildekode til smukke, delbare billeder."
+ },
+ "cssGenerators": {
+ "title": "CSS-generatorer",
+ "description": "Generatorer til box-shadow, border-radius og flydende clamp()."
+ },
+ "exifViewer": {
+ "title": "EXIF-fremviser og -fjerner",
+ "description": "Inspicér og fjern EXIF-metadata fra fotos lokalt."
+ },
+ "faviconGenerator": {
+ "title": "Favicon-generator",
+ "description": "Generér favicons, app-ikoner og manifest-uddrag."
+ },
+ "jsonDiff": {
+ "title": "JSON-diff",
+ "description": "Semantisk, strukturbevidst sammenligning af to JSON-dokumenter."
+ },
+ "jsonpathPlayground": {
+ "title": "JSONPath-legeplads",
+ "description": "Forespørg JSON med JSONPath eller JMESPath, og se resultater live."
+ },
+ "keycodeInspector": {
+ "title": "Tastekode-inspektør",
+ "description": "Tryk på en tast for at se dens JavaScript-hændelseskoder og modifikatorer."
+ },
+ "markdownTableGenerator": {
+ "title": "Markdown-tabelgenerator",
+ "description": "Byg Markdown- og HTML-tabeller i en regnearkslignende gittereditor."
+ },
+ "timezoneConverter": {
+ "title": "Tidszonekonverter",
+ "description": "Konvertér et tidspunkt på tværs af mange zoner med dagsforskydning og arbejdstidsmarkeringer."
+ },
+ "tokenCounter": {
+ "title": "LLM-token-tæller",
+ "description": "Tæl LLM-tokens, visualisér chunks, og estimér API-omkostninger."
+ },
+ "webhookTester": {
+ "title": "Webhook-tester",
+ "description": "Opfang og inspicér indgående webhook-anmodninger i realtid."
+ },
+ "websocketTester": {
+ "title": "WebSocket-tester",
+ "description": "Forbind til WebSocket-endpoints, og udveksl beskeder i realtid."
+ },
+ "whoisLookup": {
+ "title": "Whois-opslag",
+ "description": "Slå registreringsoplysninger for domæner og IP-adresser op via RDAP."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Bcrypt-generator og -kontrol",
+ "subtitle": "Hash adgangskoder med bcrypt, og verificér hashes — alt bliver i din browser",
+ "tabs": {
+ "hash": "Hash",
+ "verify": "Verificér"
+ },
+ "showPassword": "Vis adgangskode",
+ "hidePassword": "Skjul adgangskode",
+ "copy": "Kopiér",
+ "copied": "Kopieret",
+ "hash": {
+ "passwordLabel": "Adgangskode",
+ "passwordPlaceholder": "Indtast en adgangskode, der skal hashes…",
+ "costLabel": "Omkostningsfaktor",
+ "costHint": "Højere omkostning fordobler arbejdet pr. trin. 10-12 er et almindeligt valg til produktion.",
+ "slowWarning": "Omkostningsfaktorer på 13 og derover kan tage flere sekunder at beregne.",
+ "generate": "Generér hash",
+ "generating": "Hasher…",
+ "resultLabel": "Bcrypt-hash",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "Hash-anatomi",
+ "algorithm": "algoritme",
+ "cost": "omkostning",
+ "salt": "salt (22 tegn)",
+ "hash": "hash (31 tegn)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Adgangskode",
+ "passwordPlaceholder": "Indtast adgangskoden, der skal kontrolleres…",
+ "hashLabel": "Bcrypt-hash",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Indtast en adgangskode og en bcrypt-hash for at sammenligne dem.",
+ "invalidHash": "Dette ligner ikke en gyldig bcrypt-hash.",
+ "checking": "Sammenligner…",
+ "match": "Match — adgangskoden producerer denne hash.",
+ "noMatch": "Intet match — adgangskoden producerer ikke denne hash."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Chmod-beregner",
+ "subtitle": "Konvertér Unix-filtilladelser mellem afkrydsningsfelter, oktal og symbolsk notation",
+ "grid": {
+ "title": "Tilladelser",
+ "read": "Læs",
+ "write": "Skriv",
+ "execute": "Kør",
+ "owner": "Ejer",
+ "group": "Gruppe",
+ "others": "Andre"
+ },
+ "special": {
+ "title": "Specielle bits",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "Oktal",
+ "placeholder": "755 eller 4755",
+ "invalid": "Indtast 3 eller 4 oktale cifre (0-7)."
+ },
+ "symbolic": {
+ "label": "Symbolsk",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Indtast 9 tegn som rwxr-xr-x (s/S, t/T tilladt)."
+ },
+ "output": {
+ "title": "Kommando",
+ "copy": "Kopiér",
+ "copied": "Kopieret"
+ },
+ "explain": {
+ "line": "{cls} kan {perms}.",
+ "none": "{cls} har ingen tilladelser.",
+ "setuid": "Setuid: filen kører med ejerens rettigheder.",
+ "setgid": "Setgid: filen kører med gruppens rettigheder (nye filer i en mappe arver dens gruppe).",
+ "sticky": "Sticky: kun en fils ejer kan slette eller omdøbe den i denne mappe."
+ },
+ "presets": {
+ "title": "Almindelige forudindstillinger",
+ "octal": "Oktal",
+ "permissions": "Tilladelser",
+ "description": "Typisk brug",
+ "p644": "Almindelige filer: ejeren skriver, alle læser.",
+ "p755": "Eksekverbare filer og mapper: alle kan læse og gå ind.",
+ "p600": "Private filer: kun ejeren læser og skriver.",
+ "p700": "Private eksekverbare filer og mapper: kun ejeren.",
+ "p664": "Gruppe-skrivbare filer.",
+ "p775": "Gruppe-skrivbare mapper og eksekverbare filer.",
+ "p400": "Skrivebeskyttet for ejeren (f.eks. private SSH-nøgler)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Kodeskærmbillede",
+ "subtitle": "Forvandl kode til smukke, delbare billeder",
+ "languageLabel": "Sprog",
+ "autoDetect": "Automatisk registrering",
+ "backgroundLabel": "Baggrund",
+ "paddingLabel": "Udfyldning",
+ "filenameLabel": "Filnavn",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "Vinduesprikker",
+ "themeLabel": "Vinduestema",
+ "themeDark": "Mørkt",
+ "themeLight": "Lyst",
+ "scaleLabel": "Eksportskala",
+ "copyImage": "Kopiér billede",
+ "copied": "Kopieret",
+ "exportPng": "Eksportér PNG",
+ "inputLabel": "Kode",
+ "detected": "Registreret: {language}",
+ "clear": "Ryd",
+ "placeholder": "Indsæt din kode her…",
+ "previewLabel": "Forhåndsvisning",
+ "emptyPreview": "Forhåndsvisningen af din kode vises her",
+ "exportFailed": "Kunne ikke eksportere billede",
+ "copyUnsupported": "Kopiering af billeder understøttes ikke i denne browser",
+ "copyFailed": "Kunne ikke kopiere billede",
+ "backgrounds": {
+ "ocean": "Ocean",
+ "sunset": "Solnedgang",
+ "candy": "Slik",
+ "forest": "Skov",
+ "midnight": "Midnat",
+ "gold": "Guld",
+ "aurora": "Nordlys",
+ "slate": "Skifer",
+ "transparent": "Gennemsigtig"
+ }
+ },
+ "CssGenerators": {
+ "title": "CSS-generatorer",
+ "subtitle": "Box-shadow, border-radius og flydende clamp() — kopiér CSS'en.",
+ "preview": "Forhåndsvisning",
+ "output": "CSS",
+ "copy": "Kopiér",
+ "copied": "Kopieret",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "Flydende clamp()"
+ },
+ "boxShadow": {
+ "presets": "Forudindstillinger",
+ "layer": "Lag",
+ "addLayer": "Tilføj lag",
+ "removeLayer": "Fjern lag",
+ "x": "X",
+ "y": "Y",
+ "blur": "Sløring",
+ "spread": "Spredning",
+ "color": "Farve",
+ "alpha": "Alfa",
+ "inset": "Indvendig",
+ "presetNames": {
+ "subtleCard": "Diskret kort",
+ "elevated": "Hævet",
+ "hardOffset": "Hård forskydning",
+ "neonGlow": "Neonglød",
+ "inner": "Indvendig",
+ "layeredSmooth": "Lagdelt blød"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Tilstand",
+ "modes": {
+ "simple": "Pr. hjørne",
+ "blob": "Organisk klat"
+ },
+ "unit": "Enhed",
+ "linkCorners": "Sammenkæd hjørner",
+ "randomize": "Tilfældiggør",
+ "horizontal": "Vandrette radier",
+ "vertical": "Lodrette radier",
+ "corners": {
+ "tl": "Øverst til venstre",
+ "tr": "Øverst til højre",
+ "br": "Nederst til højre",
+ "bl": "Nederst til venstre"
+ }
+ },
+ "clamp": {
+ "minFont": "Min. skrift (px)",
+ "maxFont": "Maks. skrift (px)",
+ "minViewport": "Min. viewport (px)",
+ "maxViewport": "Maks. viewport (px)",
+ "rootFont": "Rodskrift (px)",
+ "viewportWidth": "Simuleret viewport",
+ "computedSize": "Beregnet størrelse",
+ "slope": "Hældning",
+ "intercept": "Skæring",
+ "previewText": "Quizdeltagerne spiste jordbær",
+ "invalid": "Indtast gyldige, forskellige viewport-grænser."
+ }
+ },
+ "ExifViewer": {
+ "title": "EXIF-fremviser og -fjerner",
+ "subtitle": "Inspicér fotos' metadata, og fjern dem i din browser",
+ "dropTitle": "Slip et billede her",
+ "dropHint": "eller klik for at gennemse — dit billede forlader aldrig din enhed",
+ "browse": "Vælg billede",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Læser metadata…",
+ "parseFailed": "Kunne ikke læse dette billede",
+ "clear": "Fjern billede",
+ "gpsWarning": "Indeholder GPS-placering",
+ "openMap": "Vis på OpenStreetMap",
+ "noMetadata": "Der blev ikke fundet nogen indlejrede metadata i dette billede.",
+ "facts": {
+ "name": "Navn",
+ "type": "Type",
+ "dimensions": "Dimensioner",
+ "size": "Størrelse"
+ },
+ "groups": {
+ "camera": "Kamera",
+ "exposure": "Eksponering",
+ "timestamps": "Tidsstempler",
+ "gps": "GPS-placering",
+ "software": "Software og andet"
+ },
+ "gpsCard": {
+ "latitude": "Breddegrad",
+ "longitude": "Længdegrad",
+ "altitude": "Højde"
+ },
+ "raw": {
+ "title": "Alle metadata",
+ "copyJson": "Kopiér JSON",
+ "copied": "Kopieret"
+ },
+ "remove": {
+ "title": "Fjern metadata",
+ "jpegDescription": "Fjern EXIF, XMP, IPTC og kommentarer tabsfrit uden at genkomprimere billedet.",
+ "reencodeDescription": "Genkod billedet for at fjerne indlejrede metadata. Dette genkomprimerer pixels.",
+ "keepIcc": "Behold farveprofil (ICC)",
+ "stripButton": "Fjern metadata",
+ "reencodeButton": "Genkod og fjern",
+ "unsupported": "Fjernelse af metadata understøttes ikke for denne filtype.",
+ "failed": "Kunne ikke fjerne metadata",
+ "before": "Før",
+ "after": "Efter",
+ "download": "Download renset billede"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Favicon-generator",
+ "subtitle": "Opret favicons og app-ikoner ud fra ét billede",
+ "dropTitle": "Slip et billede her",
+ "dropHint": "PNG, JPEG eller SVG — klik for at gennemse",
+ "browse": "Vælg billede",
+ "clear": "Fjern billede",
+ "backgroundLabel": "Baggrund",
+ "transparent": "Gennemsigtig",
+ "solidColor": "Ensfarvet",
+ "cornerLabel": "Hjørneform",
+ "cornerNone": "Firkantet",
+ "cornerRounded": "Afrundet",
+ "cornerCircle": "Cirkel",
+ "maskPaddingLabel": "Maskerbar udfyldning",
+ "maskable": "maskable",
+ "previewLabel": "Forhåndsvisning",
+ "copy": "Kopiér",
+ "headSnippet": "HTML head-uddrag",
+ "downloadIco": "Download favicon.ico",
+ "downloadZip": "Download alle (.zip)"
+ },
+ "JsonDiff": {
+ "title": "JSON-diff",
+ "subtitle": "Semantisk, strukturbevidst sammenligning af to JSON-dokumenter — nøglerækkefølge og formatering ignoreres",
+ "options": {
+ "ignoreArrayOrder": "Ignorér array-rækkefølge"
+ },
+ "loadBothSamples": "Indlæs eksempler",
+ "sample": "Eksempel",
+ "format": "Format",
+ "clear": "Ryd",
+ "formatError": "Kan ikke formatere: input er ikke gyldig JSON",
+ "placeholder": "Indsæt eller skriv JSON her…",
+ "panels": {
+ "left": "Original (venstre)",
+ "right": "Ændret (højre)",
+ "results": "Forskelle"
+ },
+ "summary": {
+ "added": "{count} tilføjet",
+ "removed": "{count} fjernet",
+ "changed": "{count} ændret"
+ },
+ "types": {
+ "added": "Tilføjet",
+ "removed": "Fjernet",
+ "changed": "Ændret"
+ },
+ "copyChanges": "Kopiér som JSON",
+ "copied": "Kopieret",
+ "empty": "Indsæt JSON i begge paneler for at se deres forskelle.",
+ "identical": "De to dokumenter er semantisk identiske."
+ },
+ "JsonpathPlayground": {
+ "title": "JSONPath-legeplads",
+ "subtitle": "Forespørg JSON-dokumenter med JSONPath eller JMESPath, og se resultater live",
+ "engineLabel": "Motor",
+ "queryLabel": "Forespørgsel",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Snydeark",
+ "cheats": {
+ "jpAllTitles": "Alle bogtitler",
+ "jpAllAuthors": "Hver forfatter, hvor som helst i dokumentet",
+ "jpFirstBook": "Første bog",
+ "jpLastBook": "Sidste bog",
+ "jpSlice": "De første to bøger (udsnit)",
+ "jpFilterPrice": "Bøger billigere end 40",
+ "jpFilterInStock": "Titler på bøger på lager",
+ "jpRecursive": "Alle værdier, rekursivt",
+ "jmAllTitles": "Alle bogtitler",
+ "jmFirstAuthor": "Forfatter af den første bog",
+ "jmLastBook": "Sidste bog",
+ "jmSlice": "De første to bøger (udsnit)",
+ "jmFilterPrice": "Titler på bøger billigere end 40",
+ "jmCount": "Antal bøger på lager",
+ "jmProjection": "Omform hver bog til et nyt objekt",
+ "jmSort": "Titler sorteret efter pris"
+ },
+ "panels": {
+ "input": "JSON-input",
+ "results": "Resultater"
+ },
+ "sample": "Eksempel",
+ "format": "Format",
+ "clear": "Ryd",
+ "formatError": "Kan ikke formatere: input er ikke gyldig JSON",
+ "placeholder": "Indsæt eller skriv JSON her…",
+ "outputPlaceholder": "Match vises her…",
+ "matchCount": "{count, plural, one {# match} other {# match}}",
+ "copy": "Kopiér",
+ "copied": "Kopieret"
+ },
+ "KeycodeInspector": {
+ "title": "Tastekode-inspektør",
+ "subtitle": "Tryk på en vilkårlig tast for at inspicere dens JavaScript-tastaturhændelse",
+ "capture": {
+ "prompt": "Tryk på en vilkårlig tast…",
+ "ready": "Lytter — tryk på en vilkårlig tast på dit tastatur",
+ "clickToFocus": "Klik her for at fokusere, og tryk derefter på en vilkårlig tast"
+ },
+ "hero": {
+ "spaceKey": "(Mellemrum)",
+ "keyLabel": "event.key",
+ "repeat": "gentagelse"
+ },
+ "copyJson": "Kopiér JSON",
+ "copiedJson": "Hændelses-JSON kopieret til udklipsholder",
+ "deprecated": "forældet",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Standard",
+ "left": "Venstre",
+ "right": "Højre",
+ "numpad": "Numerisk tastatur",
+ "unknown": "Ukendt"
+ },
+ "history": {
+ "title": "Historik",
+ "clear": "Ryd historik",
+ "empty": "Tastetryk vises her."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Markdown-tabelgenerator",
+ "subtitle": "Rediger et gitter, indsæt fra et regneark, og kopiér en pænt justeret Markdown- eller HTML-tabel",
+ "addRow": "Tilføj række",
+ "addColumn": "Tilføj kolonne",
+ "importLabel": "Importér",
+ "importPlaceholder": "Indsæt TSV (fra Excel / Google Sheets) eller CSV her…",
+ "importButton": "Importér til gitter",
+ "pasteClipboard": "Indsæt fra udklipsholder",
+ "importEmpty": "Intet at importere — indsæt nogle rækker først",
+ "clipboardError": "Kunne ikke læse fra udklipsholderen",
+ "clear": "Ryd",
+ "panels": {
+ "editor": "Tabeleditor",
+ "output": "Output"
+ },
+ "align": {
+ "left": "Justér venstre",
+ "center": "Centrér",
+ "right": "Justér højre"
+ },
+ "headerPlaceholder": "Overskrift",
+ "cellPlaceholder": "Celle",
+ "removeRow": "Fjern række",
+ "removeColumn": "Fjern kolonne",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Kopiér",
+ "copied": "Kopieret"
+ },
+ "TimezoneConverter": {
+ "title": "Tidszonekonverter",
+ "subtitle": "Konvertér ét øjeblik på tværs af verden til mødeplanlægning.",
+ "dateTimeLabel": "Dato og tid",
+ "now": "Nu",
+ "sourceZoneLabel": "Kilde-tidszone",
+ "selectZone": "Vælg zone",
+ "searchZone": "Søg tidszoner...",
+ "noZone": "Ingen tidszone fundet.",
+ "scrubLabel": "Forskyd tid",
+ "reset": "Nulstil",
+ "targetsLabel": "Målzoner",
+ "addZone": "Tilføj zone",
+ "remove": "Fjern",
+ "localBadge": "Lokal",
+ "invalidDate": "Indtast en gyldig dato og tid.",
+ "bands": {
+ "work": "Arbejdstid (09:00–18:00)",
+ "edge": "Randtimer (tidligt / aften)",
+ "night": "Nat"
+ }
+ },
+ "TokenCounter": {
+ "title": "LLM-token-tæller",
+ "subtitle": "Tæl tokens, visualisér chunks, og estimér API-omkostninger.",
+ "inputLabel": "Tekst",
+ "sample": "Eksempel",
+ "clear": "Ryd",
+ "placeholder": "Indsæt tekst, kode eller JSON for at tælle tokens...",
+ "encoding": "Kodning",
+ "loading": "Indlæser tokenizer...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / o-serien)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Tokens",
+ "characters": "Tegn",
+ "words": "Ord",
+ "ratio": "Tokens/ord",
+ "ratioSub": "cirka"
+ },
+ "claudeRow": "Claude (omtrentlig)",
+ "estimate": "estimat",
+ "viz": {
+ "title": "Token-visualisering",
+ "toggle": "Vis",
+ "hint": "Slå til for at farve hvert token som en chunk.",
+ "empty": "Indtast tekst for at visualisere tokens.",
+ "truncated": "Viser de første {limit} af {total} tokens."
+ },
+ "cost": {
+ "title": "Omkostningsberegner",
+ "disclaimer": "redigerbare standardværdier — verificér aktuelle priser",
+ "model": "Model",
+ "expectedOutput": "Forventede output-tokens",
+ "customInput": "Input $/MTok",
+ "customOutput": "Output $/MTok",
+ "inputCost": "Input-omkostning",
+ "outputCost": "Output-omkostning",
+ "total": "I alt",
+ "per1000": "Pr. 1000 kald"
+ }
+ },
+ "WebhookTester": {
+ "title": "Webhook-tester",
+ "subtitle": "Opret en midlertidig URL, og inspicér indgående webhook-anmodninger live.",
+ "intro": {
+ "title": "Opret en webhook-URL for at begynde at opfange anmodninger",
+ "hint": "Du får en ugættelig URL, der registrerer hver indgående anmodning. Den udløber efter 24 timer."
+ },
+ "createBtn": "Opret webhook-URL",
+ "creating": "Opretter…",
+ "urlLabel": "Din webhook-URL",
+ "expiresIn": "Udløber om {countdown}",
+ "copyUrlBtn": "Kopiér URL",
+ "copied": "Kopieret",
+ "clearBtn": "Ryd",
+ "newBinBtn": "Ny URL",
+ "requestsTitle": "Anmodninger",
+ "selectPrompt": "Vælg en anmodning for at inspicere dens detaljer.",
+ "expiredNotice": "Denne webhook-URL er udløbet. Opret en ny for at fortsætte med at teste.",
+ "base64Badge": "base64",
+ "truncatedBadge": "afkortet",
+ "empty": {
+ "title": "Venter på anmodninger…",
+ "hint": "Send en anmodning til din URL, så vises den her med det samme. Prøv dette:"
+ },
+ "detail": {
+ "receivedAt": "Modtaget",
+ "source": "Kilde-IP",
+ "contentType": "Content-Type",
+ "query": "Forespørgselsparametre",
+ "headers": "Headers",
+ "body": "Body",
+ "copyBody": "Kopiér body",
+ "emptyBody": "Ingen body"
+ },
+ "errors": {
+ "createFailed": "Kunne ikke oprette webhook-URL. Prøv igen.",
+ "clearFailed": "Kunne ikke rydde anmodninger. Prøv igen."
+ }
+ },
+ "WebsocketTester": {
+ "title": "WebSocket-tester",
+ "subtitle": "Forbind til et WebSocket-endpoint, og send, modtag og inspicér beskeder live",
+ "urlLabel": "Endpoint-URL",
+ "protocolsLabel": "Subprotokoller",
+ "protocolsPlaceholder": "valgfrit, kommasepareret",
+ "connect": "Forbind",
+ "disconnect": "Afbryd",
+ "status": {
+ "disconnected": "Afbrudt",
+ "connecting": "Forbinder",
+ "connected": "Forbundet",
+ "error": "Fejl"
+ },
+ "messages": {
+ "connecting": "Forbinder til {url}…",
+ "connected": "Forbindelse etableret",
+ "connectedWithProtocol": "Forbindelse etableret (protokol: {protocol})",
+ "invalidUrl": "Ugyldig URL — skal begynde med ws:// eller wss://",
+ "connectFailed": "Kunne ikke åbne forbindelsen",
+ "socketError": "Der opstod en forbindelsesfejl",
+ "notConnected": "Ikke forbundet — forbind først",
+ "binaryMessage": "Modtog en binær besked"
+ },
+ "log": {
+ "title": "Beskedlog",
+ "empty": "Ingen beskeder endnu. Forbind, og begynd at sende.",
+ "copyAll": "Kopiér alle",
+ "download": "Download som .txt",
+ "clear": "Ryd log",
+ "prettyPrint": "Formatér JSON pænt",
+ "showRaw": "Vis rå tekst"
+ },
+ "composer": {
+ "placeholder": "Skriv en besked, der skal sendes…",
+ "send": "Send",
+ "formatJson": "Formatér som JSON",
+ "invalidJson": "Beskeden er ikke gyldig JSON",
+ "hint": "Cmd/Ctrl+Enter for at sende"
+ },
+ "ping": {
+ "label": "Auto-ping",
+ "seconds": "sek",
+ "intervalTitle": "Ping-interval i sekunder",
+ "payloadPlaceholder": "ping-payload"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Whois-opslag",
+ "subtitle": "Slå registreringsoplysninger op for ethvert domæne eller IP-adresse via RDAP",
+ "placeholder": "example.com eller 8.8.8.8",
+ "lookupBtn": "Slå op",
+ "looking": "Slår op…",
+ "refreshBtn": "Opdatér",
+ "tryLabel": "Prøv:",
+ "copy": "Kopiér",
+ "copied": "Kopieret",
+ "notAvailable": "Ikke tilgængelig",
+ "summary": {
+ "registrar": "Registrator",
+ "network": "Netværk"
+ },
+ "dates": {
+ "created": "Oprettet",
+ "updated": "Opdateret",
+ "expires": "Udløber"
+ },
+ "sections": {
+ "status": "Status",
+ "nameservers": "Navneservere",
+ "contacts": "Kontakter",
+ "rawRdap": "Rå RDAP"
+ },
+ "contacts": {
+ "roles": "Roller",
+ "name": "Navn",
+ "organization": "Organisation"
+ },
+ "errors": {
+ "invalidTarget": "Indtast et gyldigt domænenavn eller IP-adresse",
+ "lookupFailed": "Opslag mislykkedes (status {status})"
+ }
}
}
diff --git a/apps/web/messages/de.json b/apps/web/messages/de.json
index ad32596e..6caa3be1 100644
--- a/apps/web/messages/de.json
+++ b/apps/web/messages/de.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Chmod-Rechner",
+ "codeScreenshot": "Code-Screenshot",
+ "cssGenerators": "CSS-Generatoren",
+ "exifViewer": "EXIF-Viewer",
+ "faviconGenerator": "Favicon-Generator",
+ "jsonDiff": "JSON-Diff",
+ "jsonpathPlayground": "JSONPath-Playground",
+ "keycodeInspector": "Keycode-Inspektor",
+ "markdownTableGenerator": "Markdown-Tabelle",
+ "timezoneConverter": "Zeitzonen-Konverter",
+ "tokenCounter": "Token-Zähler",
+ "webhookTester": "Webhook-Tester",
+ "websocketTester": "WebSocket-Tester",
+ "whoisLookup": "Whois-Abfrage"
},
"Help": {
"title": "Hilfe & Dokumentation",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Bcrypt-Generator & -Prüfer",
+ "description": "Passwörter mit bcrypt hashen und bestehende Hashes verifizieren."
+ },
+ "chmodCalculator": {
+ "title": "Chmod-Rechner",
+ "description": "Unix-Berechtigungen zwischen Oktal, symbolisch und Kontrollkästchen umrechnen."
+ },
+ "codeScreenshot": {
+ "title": "Code-Screenshot",
+ "description": "Quellcode in schöne, teilbare Bilder verwandeln."
+ },
+ "cssGenerators": {
+ "title": "CSS-Generatoren",
+ "description": "Generatoren für box-shadow, border-radius und fluides clamp()."
+ },
+ "exifViewer": {
+ "title": "EXIF-Viewer & -Entferner",
+ "description": "EXIF-Metadaten von Fotos lokal prüfen und entfernen."
+ },
+ "faviconGenerator": {
+ "title": "Favicon-Generator",
+ "description": "Favicons, App-Icons und Manifest-Snippets erzeugen."
+ },
+ "jsonDiff": {
+ "title": "JSON-Diff",
+ "description": "Semantischer, strukturbewusster Vergleich zweier JSON-Dokumente."
+ },
+ "jsonpathPlayground": {
+ "title": "JSONPath-Playground",
+ "description": "JSON mit JSONPath oder JMESPath abfragen und Ergebnisse live sehen."
+ },
+ "keycodeInspector": {
+ "title": "Keycode-Inspektor",
+ "description": "Eine Taste drücken, um ihre JavaScript-Event-Codes und Modifikatoren zu sehen."
+ },
+ "markdownTableGenerator": {
+ "title": "Markdown-Tabellen-Generator",
+ "description": "Markdown- und HTML-Tabellen in einem tabellenkalkulationsähnlichen Raster-Editor erstellen."
+ },
+ "timezoneConverter": {
+ "title": "Zeitzonen-Konverter",
+ "description": "Eine Zeit über viele Zonen umrechnen, mit Hinweisen zu Tageswechsel und Arbeitszeiten."
+ },
+ "tokenCounter": {
+ "title": "LLM-Token-Zähler",
+ "description": "LLM-Tokens zählen, Chunks visualisieren und API-Kosten schätzen."
+ },
+ "webhookTester": {
+ "title": "Webhook-Tester",
+ "description": "Eingehende Webhook-Anfragen in Echtzeit erfassen und untersuchen."
+ },
+ "websocketTester": {
+ "title": "WebSocket-Tester",
+ "description": "Mit WebSocket-Endpunkten verbinden und Nachrichten in Echtzeit austauschen."
+ },
+ "whoisLookup": {
+ "title": "Whois-Abfrage",
+ "description": "Registrierungsdaten von Domains und IPs per RDAP abrufen."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Bcrypt-Generator & -Prüfer",
+ "subtitle": "Passwörter mit bcrypt hashen und Hashes verifizieren – alles bleibt in deinem Browser",
+ "tabs": {
+ "hash": "Hashen",
+ "verify": "Verifizieren"
+ },
+ "showPassword": "Passwort anzeigen",
+ "hidePassword": "Passwort verbergen",
+ "copy": "Kopieren",
+ "copied": "Kopiert",
+ "hash": {
+ "passwordLabel": "Passwort",
+ "passwordPlaceholder": "Ein Passwort zum Hashen eingeben…",
+ "costLabel": "Kostenfaktor",
+ "costHint": "Ein höherer Kostenfaktor verdoppelt den Aufwand pro Schritt. 10-12 ist eine gängige Wahl für die Produktion.",
+ "slowWarning": "Kostenfaktoren ab 13 können mehrere Sekunden zur Berechnung benötigen.",
+ "generate": "Hash erzeugen",
+ "generating": "Hashen…",
+ "resultLabel": "Bcrypt-Hash",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "Aufbau des Hashes",
+ "algorithm": "Algorithmus",
+ "cost": "Kosten",
+ "salt": "Salt (22 Zeichen)",
+ "hash": "Hash (31 Zeichen)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Passwort",
+ "passwordPlaceholder": "Zu prüfendes Passwort eingeben…",
+ "hashLabel": "Bcrypt-Hash",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Gib ein Passwort und einen Bcrypt-Hash ein, um sie zu vergleichen.",
+ "invalidHash": "Das sieht nicht nach einem gültigen Bcrypt-Hash aus.",
+ "checking": "Vergleiche…",
+ "match": "Übereinstimmung – das Passwort erzeugt diesen Hash.",
+ "noMatch": "Keine Übereinstimmung – das Passwort erzeugt diesen Hash nicht."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Chmod-Rechner",
+ "subtitle": "Unix-Dateiberechtigungen zwischen Kontrollkästchen, Oktal und symbolischer Notation umrechnen",
+ "grid": {
+ "title": "Berechtigungen",
+ "read": "Lesen",
+ "write": "Schreiben",
+ "execute": "Ausführen",
+ "owner": "Eigentümer",
+ "group": "Gruppe",
+ "others": "Andere"
+ },
+ "special": {
+ "title": "Spezielle Bits",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "Oktal",
+ "placeholder": "755 oder 4755",
+ "invalid": "Gib 3 oder 4 Oktalziffern (0-7) ein."
+ },
+ "symbolic": {
+ "label": "Symbolisch",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Gib 9 Zeichen wie rwxr-xr-x ein (s/S, t/T erlaubt)."
+ },
+ "output": {
+ "title": "Befehl",
+ "copy": "Kopieren",
+ "copied": "Kopiert"
+ },
+ "explain": {
+ "line": "{cls} kann {perms}.",
+ "none": "{cls} hat keine Berechtigungen.",
+ "setuid": "Setuid: Die Datei läuft mit den Rechten ihres Eigentümers.",
+ "setgid": "Setgid: Die Datei läuft mit den Rechten ihrer Gruppe (neue Dateien in einem Verzeichnis erben dessen Gruppe).",
+ "sticky": "Sticky: Nur der Eigentümer einer Datei kann sie in diesem Verzeichnis löschen oder umbenennen."
+ },
+ "presets": {
+ "title": "Gängige Voreinstellungen",
+ "octal": "Oktal",
+ "permissions": "Berechtigungen",
+ "description": "Typische Verwendung",
+ "p644": "Normale Dateien: Eigentümer schreibt, alle lesen.",
+ "p755": "Ausführbare Dateien und Verzeichnisse: alle können lesen und betreten.",
+ "p600": "Private Dateien: nur der Eigentümer liest und schreibt.",
+ "p700": "Private ausführbare Dateien und Verzeichnisse: nur der Eigentümer.",
+ "p664": "Für die Gruppe schreibbare Dateien.",
+ "p775": "Für die Gruppe schreibbare Verzeichnisse und ausführbare Dateien.",
+ "p400": "Nur Lesezugriff für den Eigentümer (z. B. private SSH-Schlüssel)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Code-Screenshot",
+ "subtitle": "Code in schöne, teilbare Bilder verwandeln",
+ "languageLabel": "Sprache",
+ "autoDetect": "Automatisch erkennen",
+ "backgroundLabel": "Hintergrund",
+ "paddingLabel": "Innenabstand",
+ "filenameLabel": "Dateiname",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "Fensterpunkte",
+ "themeLabel": "Fenster-Theme",
+ "themeDark": "Dunkel",
+ "themeLight": "Hell",
+ "scaleLabel": "Export-Skalierung",
+ "copyImage": "Bild kopieren",
+ "copied": "Kopiert",
+ "exportPng": "PNG exportieren",
+ "inputLabel": "Code",
+ "detected": "Erkannt: {language}",
+ "clear": "Leeren",
+ "placeholder": "Füge deinen Code hier ein…",
+ "previewLabel": "Vorschau",
+ "emptyPreview": "Die Vorschau deines Codes erscheint hier",
+ "exportFailed": "Bild konnte nicht exportiert werden",
+ "copyUnsupported": "Das Kopieren von Bildern wird in diesem Browser nicht unterstützt",
+ "copyFailed": "Bild konnte nicht kopiert werden",
+ "backgrounds": {
+ "ocean": "Ozean",
+ "sunset": "Sonnenuntergang",
+ "candy": "Bonbon",
+ "forest": "Wald",
+ "midnight": "Mitternacht",
+ "gold": "Gold",
+ "aurora": "Polarlicht",
+ "slate": "Schiefer",
+ "transparent": "Transparent"
+ }
+ },
+ "CssGenerators": {
+ "title": "CSS-Generatoren",
+ "subtitle": "Box-shadow, border-radius und fluides clamp() – kopiere das CSS.",
+ "preview": "Vorschau",
+ "output": "CSS",
+ "copy": "Kopieren",
+ "copied": "Kopiert",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "Fluides clamp()"
+ },
+ "boxShadow": {
+ "presets": "Voreinstellungen",
+ "layer": "Ebene",
+ "addLayer": "Ebene hinzufügen",
+ "removeLayer": "Ebene entfernen",
+ "x": "X",
+ "y": "Y",
+ "blur": "Weichzeichnen",
+ "spread": "Ausbreitung",
+ "color": "Farbe",
+ "alpha": "Alpha",
+ "inset": "Innen",
+ "presetNames": {
+ "subtleCard": "Dezente Karte",
+ "elevated": "Erhöht",
+ "hardOffset": "Harter Versatz",
+ "neonGlow": "Neon-Leuchten",
+ "inner": "Innen",
+ "layeredSmooth": "Weich geschichtet"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Modus",
+ "modes": {
+ "simple": "Pro Ecke",
+ "blob": "Organischer Blob"
+ },
+ "unit": "Einheit",
+ "linkCorners": "Ecken verknüpfen",
+ "randomize": "Zufällig",
+ "horizontal": "Horizontale Radien",
+ "vertical": "Vertikale Radien",
+ "corners": {
+ "tl": "Oben links",
+ "tr": "Oben rechts",
+ "br": "Unten rechts",
+ "bl": "Unten links"
+ }
+ },
+ "clamp": {
+ "minFont": "Min. Schrift (px)",
+ "maxFont": "Max. Schrift (px)",
+ "minViewport": "Min. Viewport (px)",
+ "maxViewport": "Max. Viewport (px)",
+ "rootFont": "Wurzelschrift (px)",
+ "viewportWidth": "Simuliertes Viewport",
+ "computedSize": "Berechnete Größe",
+ "slope": "Steigung",
+ "intercept": "Achsenabschnitt",
+ "previewText": "Franz jagt im Taxi quer durch Bayern",
+ "invalid": "Gib gültige, unterschiedliche Viewport-Grenzen ein."
+ }
+ },
+ "ExifViewer": {
+ "title": "EXIF-Viewer & -Entferner",
+ "subtitle": "Foto-Metadaten prüfen und in deinem Browser entfernen",
+ "dropTitle": "Ein Bild hier ablegen",
+ "dropHint": "oder zum Durchsuchen klicken – dein Bild verlässt nie dein Gerät",
+ "browse": "Bild auswählen",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Metadaten werden gelesen…",
+ "parseFailed": "Dieses Bild konnte nicht gelesen werden",
+ "clear": "Bild entfernen",
+ "gpsWarning": "Enthält GPS-Standort",
+ "openMap": "Auf OpenStreetMap ansehen",
+ "noMetadata": "In diesem Bild wurden keine eingebetteten Metadaten gefunden.",
+ "facts": {
+ "name": "Name",
+ "type": "Typ",
+ "dimensions": "Abmessungen",
+ "size": "Größe"
+ },
+ "groups": {
+ "camera": "Kamera",
+ "exposure": "Belichtung",
+ "timestamps": "Zeitstempel",
+ "gps": "GPS-Standort",
+ "software": "Software & Sonstiges"
+ },
+ "gpsCard": {
+ "latitude": "Breitengrad",
+ "longitude": "Längengrad",
+ "altitude": "Höhe"
+ },
+ "raw": {
+ "title": "Alle Metadaten",
+ "copyJson": "JSON kopieren",
+ "copied": "Kopiert"
+ },
+ "remove": {
+ "title": "Metadaten entfernen",
+ "jpegDescription": "EXIF, XMP, IPTC und Kommentare verlustfrei entfernen, ohne das Bild neu zu komprimieren.",
+ "reencodeDescription": "Das Bild neu codieren, um eingebettete Metadaten zu entfernen. Dabei werden die Pixel neu komprimiert.",
+ "keepIcc": "Farbprofil (ICC) beibehalten",
+ "stripButton": "Metadaten entfernen",
+ "reencodeButton": "Neu codieren & entfernen",
+ "unsupported": "Das Entfernen von Metadaten wird für diesen Dateityp nicht unterstützt.",
+ "failed": "Metadaten konnten nicht entfernt werden",
+ "before": "Vorher",
+ "after": "Nachher",
+ "download": "Bereinigtes Bild herunterladen"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Favicon-Generator",
+ "subtitle": "Favicons und App-Icons aus einem einzigen Bild erstellen",
+ "dropTitle": "Ein Bild hier ablegen",
+ "dropHint": "PNG, JPEG oder SVG – zum Durchsuchen klicken",
+ "browse": "Bild auswählen",
+ "clear": "Bild entfernen",
+ "backgroundLabel": "Hintergrund",
+ "transparent": "Transparent",
+ "solidColor": "Einfarbig",
+ "cornerLabel": "Eckenform",
+ "cornerNone": "Quadratisch",
+ "cornerRounded": "Abgerundet",
+ "cornerCircle": "Kreis",
+ "maskPaddingLabel": "Maskable-Abstand",
+ "maskable": "maskable",
+ "previewLabel": "Vorschau",
+ "copy": "Kopieren",
+ "headSnippet": "HTML-head-Snippet",
+ "downloadIco": "favicon.ico herunterladen",
+ "downloadZip": "Alles herunterladen (.zip)"
+ },
+ "JsonDiff": {
+ "title": "JSON-Diff",
+ "subtitle": "Semantischer, strukturbewusster Vergleich zweier JSON-Dokumente – Schlüsselreihenfolge und Formatierung werden ignoriert",
+ "options": {
+ "ignoreArrayOrder": "Array-Reihenfolge ignorieren"
+ },
+ "loadBothSamples": "Beispiele laden",
+ "sample": "Beispiel laden",
+ "format": "Formatieren",
+ "clear": "Leeren",
+ "formatError": "Formatieren nicht möglich: Die Eingabe ist kein gültiges JSON",
+ "placeholder": "JSON hier einfügen oder eingeben…",
+ "panels": {
+ "left": "Original (links)",
+ "right": "Geändert (rechts)",
+ "results": "Unterschiede"
+ },
+ "summary": {
+ "added": "{count} hinzugefügt",
+ "removed": "{count} entfernt",
+ "changed": "{count} geändert"
+ },
+ "types": {
+ "added": "Hinzugefügt",
+ "removed": "Entfernt",
+ "changed": "Geändert"
+ },
+ "copyChanges": "Als JSON kopieren",
+ "copied": "Kopiert",
+ "empty": "Füge JSON in beide Panels ein, um ihre Unterschiede zu sehen.",
+ "identical": "Die beiden Dokumente sind semantisch identisch."
+ },
+ "JsonpathPlayground": {
+ "title": "JSONPath-Playground",
+ "subtitle": "JSON-Dokumente mit JSONPath oder JMESPath abfragen und Ergebnisse live sehen",
+ "engineLabel": "Engine",
+ "queryLabel": "Abfrage",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Spickzettel",
+ "cheats": {
+ "jpAllTitles": "Alle Buchtitel",
+ "jpAllAuthors": "Jeder Autor, überall im Dokument",
+ "jpFirstBook": "Erstes Buch",
+ "jpLastBook": "Letztes Buch",
+ "jpSlice": "Die ersten zwei Bücher (Slice)",
+ "jpFilterPrice": "Bücher günstiger als 40",
+ "jpFilterInStock": "Titel der vorrätigen Bücher",
+ "jpRecursive": "Alle Werte, rekursiv",
+ "jmAllTitles": "Alle Buchtitel",
+ "jmFirstAuthor": "Autor des ersten Buches",
+ "jmLastBook": "Letztes Buch",
+ "jmSlice": "Die ersten zwei Bücher (Slice)",
+ "jmFilterPrice": "Titel der Bücher günstiger als 40",
+ "jmCount": "Vorrätige Bücher zählen",
+ "jmProjection": "Jedes Buch in ein neues Objekt umformen",
+ "jmSort": "Nach Preis sortierte Titel"
+ },
+ "panels": {
+ "input": "JSON-Eingabe",
+ "results": "Ergebnisse"
+ },
+ "sample": "Beispiel laden",
+ "format": "Formatieren",
+ "clear": "Leeren",
+ "formatError": "Formatieren nicht möglich: Die Eingabe ist kein gültiges JSON",
+ "placeholder": "JSON hier einfügen oder eingeben…",
+ "outputPlaceholder": "Treffer erscheinen hier…",
+ "matchCount": "{count, plural, one {# Treffer} other {# Treffer}}",
+ "copy": "Kopieren",
+ "copied": "Kopiert"
+ },
+ "KeycodeInspector": {
+ "title": "Keycode-Inspektor",
+ "subtitle": "Drücke eine beliebige Taste, um ihr JavaScript-Tastaturevent zu untersuchen",
+ "capture": {
+ "prompt": "Drücke eine beliebige Taste…",
+ "ready": "Höre zu – drücke eine beliebige Taste auf deiner Tastatur",
+ "clickToFocus": "Klicke hier, um zu fokussieren, und drücke dann eine beliebige Taste"
+ },
+ "hero": {
+ "spaceKey": "(Leertaste)",
+ "keyLabel": "event.key",
+ "repeat": "Wiederholung"
+ },
+ "copyJson": "JSON kopieren",
+ "copiedJson": "Event-JSON in die Zwischenablage kopiert",
+ "deprecated": "veraltet",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Standard",
+ "left": "Links",
+ "right": "Rechts",
+ "numpad": "Ziffernblock",
+ "unknown": "Unbekannt"
+ },
+ "history": {
+ "title": "Verlauf",
+ "clear": "Verlauf löschen",
+ "empty": "Tastendrücke erscheinen hier."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Markdown-Tabellen-Generator",
+ "subtitle": "Ein Raster bearbeiten, aus einer Tabellenkalkulation einfügen und eine hübsch ausgerichtete Markdown- oder HTML-Tabelle kopieren",
+ "addRow": "Zeile hinzufügen",
+ "addColumn": "Spalte hinzufügen",
+ "importLabel": "Importieren",
+ "importPlaceholder": "TSV (aus Excel / Google Sheets) oder CSV hier einfügen…",
+ "importButton": "Ins Raster importieren",
+ "pasteClipboard": "Aus Zwischenablage einfügen",
+ "importEmpty": "Nichts zu importieren – füge zuerst ein paar Zeilen ein",
+ "clipboardError": "Zwischenablage konnte nicht gelesen werden",
+ "clear": "Leeren",
+ "panels": {
+ "editor": "Tabelleneditor",
+ "output": "Ausgabe"
+ },
+ "align": {
+ "left": "Linksbündig",
+ "center": "Zentriert",
+ "right": "Rechtsbündig"
+ },
+ "headerPlaceholder": "Kopfzeile",
+ "cellPlaceholder": "Zelle",
+ "removeRow": "Zeile entfernen",
+ "removeColumn": "Spalte entfernen",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Kopieren",
+ "copied": "Kopiert"
+ },
+ "TimezoneConverter": {
+ "title": "Zeitzonen-Konverter",
+ "subtitle": "Einen Moment weltweit umrechnen, um Meetings zu planen.",
+ "dateTimeLabel": "Datum & Uhrzeit",
+ "now": "Jetzt",
+ "sourceZoneLabel": "Ausgangszeitzone",
+ "selectZone": "Zone auswählen",
+ "searchZone": "Zeitzonen suchen...",
+ "noZone": "Keine Zeitzone gefunden.",
+ "scrubLabel": "Zeit verschieben",
+ "reset": "Zurücksetzen",
+ "targetsLabel": "Zielzonen",
+ "addZone": "Zone hinzufügen",
+ "remove": "Entfernen",
+ "localBadge": "Lokal",
+ "invalidDate": "Gib ein gültiges Datum und eine gültige Uhrzeit ein.",
+ "bands": {
+ "work": "Arbeitszeiten (09:00–18:00)",
+ "edge": "Randzeiten (früh / abends)",
+ "night": "Nacht"
+ }
+ },
+ "TokenCounter": {
+ "title": "LLM-Token-Zähler",
+ "subtitle": "Tokens zählen, Chunks visualisieren und API-Kosten schätzen.",
+ "inputLabel": "Text",
+ "sample": "Beispiel laden",
+ "clear": "Leeren",
+ "placeholder": "Text, Code oder JSON einfügen, um Tokens zu zählen...",
+ "encoding": "Codierung",
+ "loading": "Tokenizer wird geladen...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / o-Serie)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Tokens",
+ "characters": "Zeichen",
+ "words": "Wörter",
+ "ratio": "Tokens/Wort",
+ "ratioSub": "ca."
+ },
+ "claudeRow": "Claude (näherungsweise)",
+ "estimate": "Schätzung",
+ "viz": {
+ "title": "Token-Visualisierung",
+ "toggle": "Anzeigen",
+ "hint": "Umschalten, um jedes Token als Chunk einzufärben.",
+ "empty": "Gib Text ein, um Tokens zu visualisieren.",
+ "truncated": "Zeigt die ersten {limit} von {total} Tokens."
+ },
+ "cost": {
+ "title": "Kostenrechner",
+ "disclaimer": "editierbare Standardwerte – aktuelle Preise prüfen",
+ "model": "Modell",
+ "expectedOutput": "Erwartete Ausgabe-Tokens",
+ "customInput": "Eingabe $/MTok",
+ "customOutput": "Ausgabe $/MTok",
+ "inputCost": "Eingabekosten",
+ "outputCost": "Ausgabekosten",
+ "total": "Gesamt",
+ "per1000": "Pro 1000 Aufrufe"
+ }
+ },
+ "WebhookTester": {
+ "title": "Webhook-Tester",
+ "subtitle": "Eine temporäre URL erstellen und eingehende Webhook-Anfragen live untersuchen.",
+ "intro": {
+ "title": "Erstelle eine Webhook-URL, um Anfragen zu erfassen",
+ "hint": "Du erhältst eine nicht erratbare URL, die jede eingehende Anfrage aufzeichnet. Sie läuft nach 24 Stunden ab."
+ },
+ "createBtn": "Webhook-URL erstellen",
+ "creating": "Wird erstellt…",
+ "urlLabel": "Deine Webhook-URL",
+ "expiresIn": "Läuft ab in {countdown}",
+ "copyUrlBtn": "URL kopieren",
+ "copied": "Kopiert",
+ "clearBtn": "Leeren",
+ "newBinBtn": "Neue URL",
+ "requestsTitle": "Anfragen",
+ "selectPrompt": "Wähle eine Anfrage aus, um ihre Details zu untersuchen.",
+ "expiredNotice": "Diese Webhook-URL ist abgelaufen. Erstelle eine neue, um weiter zu testen.",
+ "base64Badge": "base64",
+ "truncatedBadge": "gekürzt",
+ "empty": {
+ "title": "Warte auf Anfragen…",
+ "hint": "Sende eine Anfrage an deine URL und sie erscheint hier sofort. Probiere das:"
+ },
+ "detail": {
+ "receivedAt": "Empfangen",
+ "source": "Quell-IP",
+ "contentType": "Content-Type",
+ "query": "Query-Parameter",
+ "headers": "Header",
+ "body": "Body",
+ "copyBody": "Body kopieren",
+ "emptyBody": "Kein Body"
+ },
+ "errors": {
+ "createFailed": "Webhook-URL konnte nicht erstellt werden. Bitte versuche es erneut.",
+ "clearFailed": "Anfragen konnten nicht gelöscht werden. Bitte versuche es erneut."
+ }
+ },
+ "WebsocketTester": {
+ "title": "WebSocket-Tester",
+ "subtitle": "Mit einem WebSocket-Endpunkt verbinden und Nachrichten live senden, empfangen und untersuchen",
+ "urlLabel": "Endpunkt-URL",
+ "protocolsLabel": "Subprotokolle",
+ "protocolsPlaceholder": "optional, durch Kommas getrennt",
+ "connect": "Verbinden",
+ "disconnect": "Trennen",
+ "status": {
+ "disconnected": "Getrennt",
+ "connecting": "Verbinde",
+ "connected": "Verbunden",
+ "error": "Fehler"
+ },
+ "messages": {
+ "connecting": "Verbinde mit {url}…",
+ "connected": "Verbindung hergestellt",
+ "connectedWithProtocol": "Verbindung hergestellt (Protokoll: {protocol})",
+ "invalidUrl": "Ungültige URL – muss mit ws:// oder wss:// beginnen",
+ "connectFailed": "Verbindung konnte nicht geöffnet werden",
+ "socketError": "Es ist ein Verbindungsfehler aufgetreten",
+ "notConnected": "Nicht verbunden – zuerst verbinden",
+ "binaryMessage": "Eine Binärnachricht empfangen"
+ },
+ "log": {
+ "title": "Nachrichtenprotokoll",
+ "empty": "Noch keine Nachrichten. Verbinde dich und beginne zu senden.",
+ "copyAll": "Alle kopieren",
+ "download": "Als .txt herunterladen",
+ "clear": "Protokoll leeren",
+ "prettyPrint": "JSON formatieren",
+ "showRaw": "Rohtext anzeigen"
+ },
+ "composer": {
+ "placeholder": "Eine Nachricht zum Senden eingeben…",
+ "send": "Senden",
+ "formatJson": "Als JSON formatieren",
+ "invalidJson": "Die Nachricht ist kein gültiges JSON",
+ "hint": "Cmd/Ctrl+Enter zum Senden"
+ },
+ "ping": {
+ "label": "Auto-Ping",
+ "seconds": "Sek.",
+ "intervalTitle": "Ping-Intervall in Sekunden",
+ "payloadPlaceholder": "Ping-Payload"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Whois-Abfrage",
+ "subtitle": "Registrierungsdaten einer beliebigen Domain oder IP-Adresse per RDAP abrufen",
+ "placeholder": "example.com oder 8.8.8.8",
+ "lookupBtn": "Abfragen",
+ "looking": "Abfrage läuft…",
+ "refreshBtn": "Aktualisieren",
+ "tryLabel": "Probiere:",
+ "copy": "Kopieren",
+ "copied": "Kopiert",
+ "notAvailable": "Nicht verfügbar",
+ "summary": {
+ "registrar": "Registrar",
+ "network": "Netzwerk"
+ },
+ "dates": {
+ "created": "Erstellt",
+ "updated": "Aktualisiert",
+ "expires": "Läuft ab"
+ },
+ "sections": {
+ "status": "Status",
+ "nameservers": "Nameserver",
+ "contacts": "Kontakte",
+ "rawRdap": "Rohes RDAP"
+ },
+ "contacts": {
+ "roles": "Rollen",
+ "name": "Name",
+ "organization": "Organisation"
+ },
+ "errors": {
+ "invalidTarget": "Gib einen gültigen Domainnamen oder eine gültige IP-Adresse ein",
+ "lookupFailed": "Abfrage fehlgeschlagen (Status {status})"
+ }
}
}
diff --git a/apps/web/messages/el.json b/apps/web/messages/el.json
index ccc67057..535906f6 100644
--- a/apps/web/messages/el.json
+++ b/apps/web/messages/el.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Υπολογιστής chmod",
+ "codeScreenshot": "Στιγμιότυπο κώδικα",
+ "cssGenerators": "Γεννήτριες CSS",
+ "exifViewer": "Προβολή EXIF",
+ "faviconGenerator": "Γεννήτρια favicon",
+ "jsonDiff": "Σύγκριση JSON",
+ "jsonpathPlayground": "Παιχνιδότοπος JSONPath",
+ "keycodeInspector": "Επιθεωρητής πλήκτρων",
+ "markdownTableGenerator": "Πίνακας Markdown",
+ "timezoneConverter": "Μετατροπέας ζωνών ώρας",
+ "tokenCounter": "Μετρητής token",
+ "webhookTester": "Δοκιμαστής webhook",
+ "websocketTester": "Δοκιμαστής WebSocket",
+ "whoisLookup": "Αναζήτηση Whois"
},
"Help": {
"title": "Βοήθεια και τεκμηρίωση",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Γεννήτρια και ελεγκτής Bcrypt",
+ "description": "Κατακερματίστε κωδικούς με bcrypt και επαληθεύστε υπάρχοντες κατακερματισμούς."
+ },
+ "chmodCalculator": {
+ "title": "Υπολογιστής chmod",
+ "description": "Μετατρέψτε δικαιώματα Unix μεταξύ οκταδικής, συμβολικής μορφής και πλαισίων ελέγχου."
+ },
+ "codeScreenshot": {
+ "title": "Στιγμιότυπο κώδικα",
+ "description": "Μετατρέψτε τον πηγαίο κώδικα σε όμορφες εικόνες για κοινή χρήση."
+ },
+ "cssGenerators": {
+ "title": "Γεννήτριες CSS",
+ "description": "Γεννήτριες box-shadow, border-radius και ρευστού clamp()."
+ },
+ "exifViewer": {
+ "title": "Προβολή και αφαίρεση EXIF",
+ "description": "Επιθεωρήστε και αφαιρέστε τα μεταδεδομένα EXIF φωτογραφιών τοπικά."
+ },
+ "faviconGenerator": {
+ "title": "Γεννήτρια favicon",
+ "description": "Δημιουργήστε favicons, εικονίδια εφαρμογών και αποσπάσματα manifest."
+ },
+ "jsonDiff": {
+ "title": "Σύγκριση JSON",
+ "description": "Σημασιολογική σύγκριση δύο εγγράφων JSON με επίγνωση της δομής."
+ },
+ "jsonpathPlayground": {
+ "title": "Παιχνιδότοπος JSONPath",
+ "description": "Κάντε ερωτήματα σε JSON με JSONPath ή JMESPath και δείτε ζωντανά αποτελέσματα."
+ },
+ "keycodeInspector": {
+ "title": "Επιθεωρητής πλήκτρων",
+ "description": "Πατήστε ένα πλήκτρο για να δείτε τους κωδικούς συμβάντων JavaScript και τους τροποποιητές του."
+ },
+ "markdownTableGenerator": {
+ "title": "Γεννήτρια πινάκων Markdown",
+ "description": "Δημιουργήστε πίνακες Markdown και HTML σε έναν επεξεργαστή πλέγματος τύπου υπολογιστικού φύλλου."
+ },
+ "timezoneConverter": {
+ "title": "Μετατροπέας ζωνών ώρας",
+ "description": "Μετατρέψτε μια ώρα σε πολλές ζώνες με ενδείξεις μετατόπισης ημέρας και ωραρίου εργασίας."
+ },
+ "tokenCounter": {
+ "title": "Μετρητής token LLM",
+ "description": "Μετρήστε token LLM, οπτικοποιήστε τμήματα και εκτιμήστε το κόστος API."
+ },
+ "webhookTester": {
+ "title": "Δοκιμαστής webhook",
+ "description": "Καταγράψτε και επιθεωρήστε εισερχόμενα αιτήματα webhook σε πραγματικό χρόνο."
+ },
+ "websocketTester": {
+ "title": "Δοκιμαστής WebSocket",
+ "description": "Συνδεθείτε σε άκρα WebSocket και ανταλλάξτε μηνύματα σε πραγματικό χρόνο."
+ },
+ "whoisLookup": {
+ "title": "Αναζήτηση Whois",
+ "description": "Αναζητήστε στοιχεία καταχώρισης τομέα και IP μέσω RDAP."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Γεννήτρια και ελεγκτής Bcrypt",
+ "subtitle": "Κατακερματίστε κωδικούς με bcrypt και επαληθεύστε κατακερματισμούς — όλα παραμένουν στο πρόγραμμα περιήγησής σας",
+ "tabs": {
+ "hash": "Κατακερματισμός",
+ "verify": "Επαλήθευση"
+ },
+ "showPassword": "Εμφάνιση κωδικού",
+ "hidePassword": "Απόκρυψη κωδικού",
+ "copy": "Αντιγραφή",
+ "copied": "Αντιγράφηκε",
+ "hash": {
+ "passwordLabel": "Κωδικός",
+ "passwordPlaceholder": "Εισαγάγετε έναν κωδικό για κατακερματισμό…",
+ "costLabel": "Συντελεστής κόστους",
+ "costHint": "Υψηλότερο κόστος διπλασιάζει την εργασία ανά βήμα. Το 10–12 είναι συνήθης επιλογή για παραγωγή.",
+ "slowWarning": "Συντελεστές κόστους 13 και άνω μπορεί να χρειαστούν αρκετά δευτερόλεπτα για υπολογισμό.",
+ "generate": "Δημιουργία κατακερματισμού",
+ "generating": "Κατακερματισμός…",
+ "resultLabel": "Κατακερματισμός Bcrypt",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "Ανατομία κατακερματισμού",
+ "algorithm": "αλγόριθμος",
+ "cost": "κόστος",
+ "salt": "salt (22 χαρακτήρες)",
+ "hash": "hash (31 χαρακτήρες)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Κωδικός",
+ "passwordPlaceholder": "Εισαγάγετε τον κωδικό προς έλεγχο…",
+ "hashLabel": "Κατακερματισμός Bcrypt",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Εισαγάγετε έναν κωδικό και έναν κατακερματισμό bcrypt για να τους συγκρίνετε.",
+ "invalidHash": "Αυτό δεν φαίνεται να είναι έγκυρος κατακερματισμός bcrypt.",
+ "checking": "Σύγκριση…",
+ "match": "Ταιριάζει — ο κωδικός παράγει αυτόν τον κατακερματισμό.",
+ "noMatch": "Δεν ταιριάζει — ο κωδικός δεν παράγει αυτόν τον κατακερματισμό."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Υπολογιστής chmod",
+ "subtitle": "Μετατρέψτε δικαιώματα αρχείων Unix μεταξύ πλαισίων ελέγχου, οκταδικής και συμβολικής σημειογραφίας",
+ "grid": {
+ "title": "Δικαιώματα",
+ "read": "Ανάγνωση",
+ "write": "Εγγραφή",
+ "execute": "Εκτέλεση",
+ "owner": "Ιδιοκτήτης",
+ "group": "Ομάδα",
+ "others": "Άλλοι"
+ },
+ "special": {
+ "title": "Ειδικά bit",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "Οκταδικό",
+ "placeholder": "755 ή 4755",
+ "invalid": "Εισαγάγετε 3 ή 4 οκταδικά ψηφία (0–7)."
+ },
+ "symbolic": {
+ "label": "Συμβολικό",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Εισαγάγετε 9 χαρακτήρες όπως rwxr-xr-x (επιτρέπονται s/S, t/T)."
+ },
+ "output": {
+ "title": "Εντολή",
+ "copy": "Αντιγραφή",
+ "copied": "Αντιγράφηκε"
+ },
+ "explain": {
+ "line": "{cls} μπορεί {perms}.",
+ "none": "{cls} δεν έχει δικαιώματα.",
+ "setuid": "Setuid: το αρχείο εκτελείται με τα προνόμια του ιδιοκτήτη του.",
+ "setgid": "Setgid: το αρχείο εκτελείται με τα προνόμια της ομάδας του (νέα αρχεία σε έναν κατάλογο κληρονομούν την ομάδα του).",
+ "sticky": "Sticky: μόνο ο ιδιοκτήτης ενός αρχείου μπορεί να το διαγράψει ή να το μετονομάσει μέσα σε αυτόν τον κατάλογο."
+ },
+ "presets": {
+ "title": "Κοινές προεπιλογές",
+ "octal": "Οκταδικό",
+ "permissions": "Δικαιώματα",
+ "description": "Τυπική χρήση",
+ "p644": "Κανονικά αρχεία: ο ιδιοκτήτης γράφει, όλοι διαβάζουν.",
+ "p755": "Εκτελέσιμα και κατάλογοι: όλοι μπορούν να διαβάσουν και να εισέλθουν.",
+ "p600": "Ιδιωτικά αρχεία: μόνο ο ιδιοκτήτης διαβάζει και γράφει.",
+ "p700": "Ιδιωτικά εκτελέσιμα και κατάλογοι: μόνο ο ιδιοκτήτης.",
+ "p664": "Αρχεία εγγράψιμα από την ομάδα.",
+ "p775": "Κατάλογοι και εκτελέσιμα εγγράψιμα από την ομάδα.",
+ "p400": "Μόνο για ανάγνωση από τον ιδιοκτήτη (π.χ. ιδιωτικά κλειδιά SSH)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Στιγμιότυπο κώδικα",
+ "subtitle": "Μετατρέψτε τον κώδικα σε όμορφες εικόνες για κοινή χρήση",
+ "languageLabel": "Γλώσσα",
+ "autoDetect": "Αυτόματη ανίχνευση",
+ "backgroundLabel": "Φόντο",
+ "paddingLabel": "Εσωτερικό περιθώριο",
+ "filenameLabel": "Όνομα αρχείου",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "Κουκκίδες παραθύρου",
+ "themeLabel": "Θέμα παραθύρου",
+ "themeDark": "Σκούρο",
+ "themeLight": "Ανοιχτό",
+ "scaleLabel": "Κλίμακα εξαγωγής",
+ "copyImage": "Αντιγραφή εικόνας",
+ "copied": "Αντιγράφηκε",
+ "exportPng": "Εξαγωγή PNG",
+ "inputLabel": "Κώδικας",
+ "detected": "Ανιχνεύθηκε: {language}",
+ "clear": "Καθαρισμός",
+ "placeholder": "Επικολλήστε τον κώδικά σας εδώ…",
+ "previewLabel": "Προεπισκόπηση",
+ "emptyPreview": "Η προεπισκόπηση του κώδικά σας θα εμφανιστεί εδώ",
+ "exportFailed": "Η εξαγωγή της εικόνας απέτυχε",
+ "copyUnsupported": "Η αντιγραφή εικόνων δεν υποστηρίζεται σε αυτό το πρόγραμμα περιήγησης",
+ "copyFailed": "Η αντιγραφή της εικόνας απέτυχε",
+ "backgrounds": {
+ "ocean": "Ωκεανός",
+ "sunset": "Ηλιοβασίλεμα",
+ "candy": "Καραμέλα",
+ "forest": "Δάσος",
+ "midnight": "Μεσάνυχτα",
+ "gold": "Χρυσό",
+ "aurora": "Σέλας",
+ "slate": "Σχιστόλιθος",
+ "transparent": "Διαφανές"
+ }
+ },
+ "CssGenerators": {
+ "title": "Γεννήτριες CSS",
+ "subtitle": "Box-shadow, border-radius και ρευστό clamp() — αντιγράψτε το CSS.",
+ "preview": "Προεπισκόπηση",
+ "output": "CSS",
+ "copy": "Αντιγραφή",
+ "copied": "Αντιγράφηκε",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "Ρευστό clamp()"
+ },
+ "boxShadow": {
+ "presets": "Προεπιλογές",
+ "layer": "Επίπεδο",
+ "addLayer": "Προσθήκη επιπέδου",
+ "removeLayer": "Αφαίρεση επιπέδου",
+ "x": "X",
+ "y": "Y",
+ "blur": "Θόλωση",
+ "spread": "Διάχυση",
+ "color": "Χρώμα",
+ "alpha": "Άλφα",
+ "inset": "Εσωτερικό",
+ "presetNames": {
+ "subtleCard": "Διακριτική κάρτα",
+ "elevated": "Ανυψωμένο",
+ "hardOffset": "Έντονη μετατόπιση",
+ "neonGlow": "Λάμψη νέον",
+ "inner": "Εσωτερικό",
+ "layeredSmooth": "Πολυεπίπεδο απαλό"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Λειτουργία",
+ "modes": {
+ "simple": "Ανά γωνία",
+ "blob": "Οργανικός λεκές"
+ },
+ "unit": "Μονάδα",
+ "linkCorners": "Σύνδεση γωνιών",
+ "randomize": "Τυχαιοποίηση",
+ "horizontal": "Οριζόντιες ακτίνες",
+ "vertical": "Κατακόρυφες ακτίνες",
+ "corners": {
+ "tl": "Πάνω αριστερά",
+ "tr": "Πάνω δεξιά",
+ "br": "Κάτω δεξιά",
+ "bl": "Κάτω αριστερά"
+ }
+ },
+ "clamp": {
+ "minFont": "Ελάχ. γραμματοσειρά (px)",
+ "maxFont": "Μέγ. γραμματοσειρά (px)",
+ "minViewport": "Ελάχ. viewport (px)",
+ "maxViewport": "Μέγ. viewport (px)",
+ "rootFont": "Ριζική γραμματοσειρά (px)",
+ "viewportWidth": "Προσομοιωμένο viewport",
+ "computedSize": "Υπολογισμένο μέγεθος",
+ "slope": "Κλίση",
+ "intercept": "Τεταγμένη επί την αρχή",
+ "previewText": "Ξεσκεπάζω την ψυχοφθόρα βδελυγμία",
+ "invalid": "Εισαγάγετε έγκυρα, διαφορετικά όρια viewport."
+ }
+ },
+ "ExifViewer": {
+ "title": "Προβολή και αφαίρεση EXIF",
+ "subtitle": "Επιθεωρήστε τα μεταδεδομένα φωτογραφιών και αφαιρέστε τα στο πρόγραμμα περιήγησής σας",
+ "dropTitle": "Αποθέστε μια εικόνα εδώ",
+ "dropHint": "ή κάντε κλικ για αναζήτηση — η εικόνα σας δεν φεύγει ποτέ από τη συσκευή σας",
+ "browse": "Επιλογή εικόνας",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Ανάγνωση μεταδεδομένων…",
+ "parseFailed": "Η ανάγνωση αυτής της εικόνας απέτυχε",
+ "clear": "Αφαίρεση εικόνας",
+ "gpsWarning": "Περιέχει τοποθεσία GPS",
+ "openMap": "Προβολή στο OpenStreetMap",
+ "noMetadata": "Δεν βρέθηκαν ενσωματωμένα μεταδεδομένα σε αυτήν την εικόνα.",
+ "facts": {
+ "name": "Όνομα",
+ "type": "Τύπος",
+ "dimensions": "Διαστάσεις",
+ "size": "Μέγεθος"
+ },
+ "groups": {
+ "camera": "Κάμερα",
+ "exposure": "Έκθεση",
+ "timestamps": "Χρονικές σημάνσεις",
+ "gps": "Τοποθεσία GPS",
+ "software": "Λογισμικό και άλλα"
+ },
+ "gpsCard": {
+ "latitude": "Γεωγραφικό πλάτος",
+ "longitude": "Γεωγραφικό μήκος",
+ "altitude": "Υψόμετρο"
+ },
+ "raw": {
+ "title": "Όλα τα μεταδεδομένα",
+ "copyJson": "Αντιγραφή JSON",
+ "copied": "Αντιγράφηκε"
+ },
+ "remove": {
+ "title": "Αφαίρεση μεταδεδομένων",
+ "jpegDescription": "Αφαιρέστε χωρίς απώλειες EXIF, XMP, IPTC και σχόλια χωρίς επανασυμπίεση της εικόνας.",
+ "reencodeDescription": "Επανακωδικοποιήστε την εικόνα για να αφαιρεθούν τα ενσωματωμένα μεταδεδομένα. Αυτό επανασυμπιέζει τα pixel.",
+ "keepIcc": "Διατήρηση προφίλ χρώματος (ICC)",
+ "stripButton": "Αφαίρεση μεταδεδομένων",
+ "reencodeButton": "Επανακωδικοποίηση και αφαίρεση",
+ "unsupported": "Η αφαίρεση μεταδεδομένων δεν υποστηρίζεται για αυτόν τον τύπο αρχείου.",
+ "failed": "Η αφαίρεση μεταδεδομένων απέτυχε",
+ "before": "Πριν",
+ "after": "Μετά",
+ "download": "Λήψη καθαρισμένης εικόνας"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Γεννήτρια favicon",
+ "subtitle": "Δημιουργήστε favicons και εικονίδια εφαρμογών από μία εικόνα",
+ "dropTitle": "Αποθέστε μια εικόνα εδώ",
+ "dropHint": "PNG, JPEG ή SVG — κάντε κλικ για αναζήτηση",
+ "browse": "Επιλογή εικόνας",
+ "clear": "Αφαίρεση εικόνας",
+ "backgroundLabel": "Φόντο",
+ "transparent": "Διαφανές",
+ "solidColor": "Συμπαγές",
+ "cornerLabel": "Σχήμα γωνίας",
+ "cornerNone": "Τετράγωνο",
+ "cornerRounded": "Στρογγυλεμένο",
+ "cornerCircle": "Κύκλος",
+ "maskPaddingLabel": "Περιθώριο maskable",
+ "maskable": "maskable",
+ "previewLabel": "Προεπισκόπηση",
+ "copy": "Αντιγραφή",
+ "headSnippet": "Απόσπασμα HTML head",
+ "downloadIco": "Λήψη favicon.ico",
+ "downloadZip": "Λήψη όλων (.zip)"
+ },
+ "JsonDiff": {
+ "title": "Σύγκριση JSON",
+ "subtitle": "Σημασιολογική σύγκριση δύο εγγράφων JSON με επίγνωση της δομής — η σειρά κλειδιών και η μορφοποίηση αγνοούνται",
+ "options": {
+ "ignoreArrayOrder": "Αγνόηση σειράς πινάκων"
+ },
+ "loadBothSamples": "Φόρτωση δειγμάτων",
+ "sample": "Δείγμα",
+ "format": "Μορφοποίηση",
+ "clear": "Καθαρισμός",
+ "formatError": "Αδυναμία μορφοποίησης: η είσοδος δεν είναι έγκυρο JSON",
+ "placeholder": "Επικολλήστε ή πληκτρολογήστε JSON εδώ…",
+ "panels": {
+ "left": "Πρωτότυπο (αριστερά)",
+ "right": "Τροποποιημένο (δεξιά)",
+ "results": "Διαφορές"
+ },
+ "summary": {
+ "added": "{count} προστέθηκαν",
+ "removed": "{count} αφαιρέθηκαν",
+ "changed": "{count} άλλαξαν"
+ },
+ "types": {
+ "added": "Προστέθηκε",
+ "removed": "Αφαιρέθηκε",
+ "changed": "Άλλαξε"
+ },
+ "copyChanges": "Αντιγραφή ως JSON",
+ "copied": "Αντιγράφηκε",
+ "empty": "Επικολλήστε JSON και στα δύο πλαίσια για να δείτε τις διαφορές τους.",
+ "identical": "Τα δύο έγγραφα είναι σημασιολογικά πανομοιότυπα."
+ },
+ "JsonpathPlayground": {
+ "title": "Παιχνιδότοπος JSONPath",
+ "subtitle": "Κάντε ερωτήματα σε έγγραφα JSON με JSONPath ή JMESPath και δείτε ζωντανά αποτελέσματα",
+ "engineLabel": "Μηχανή",
+ "queryLabel": "Ερώτημα",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Σύντομος οδηγός",
+ "cheats": {
+ "jpAllTitles": "Όλοι οι τίτλοι βιβλίων",
+ "jpAllAuthors": "Κάθε συγγραφέας, οπουδήποτε στο έγγραφο",
+ "jpFirstBook": "Πρώτο βιβλίο",
+ "jpLastBook": "Τελευταίο βιβλίο",
+ "jpSlice": "Τα δύο πρώτα βιβλία (τμήμα)",
+ "jpFilterPrice": "Βιβλία φθηνότερα από 40",
+ "jpFilterInStock": "Τίτλοι διαθέσιμων βιβλίων",
+ "jpRecursive": "Όλες οι τιμές, αναδρομικά",
+ "jmAllTitles": "Όλοι οι τίτλοι βιβλίων",
+ "jmFirstAuthor": "Συγγραφέας του πρώτου βιβλίου",
+ "jmLastBook": "Τελευταίο βιβλίο",
+ "jmSlice": "Τα δύο πρώτα βιβλία (τμήμα)",
+ "jmFilterPrice": "Τίτλοι βιβλίων φθηνότερων από 40",
+ "jmCount": "Πλήθος διαθέσιμων βιβλίων",
+ "jmProjection": "Αναδιαμορφώστε κάθε βιβλίο σε νέο αντικείμενο",
+ "jmSort": "Τίτλοι ταξινομημένοι κατά τιμή"
+ },
+ "panels": {
+ "input": "Είσοδος JSON",
+ "results": "Αποτελέσματα"
+ },
+ "sample": "Δείγμα",
+ "format": "Μορφοποίηση",
+ "clear": "Καθαρισμός",
+ "formatError": "Αδυναμία μορφοποίησης: η είσοδος δεν είναι έγκυρο JSON",
+ "placeholder": "Επικολλήστε ή πληκτρολογήστε JSON εδώ…",
+ "outputPlaceholder": "Οι αντιστοιχίσεις θα εμφανιστούν εδώ…",
+ "matchCount": "{count, plural, one {# αντιστοίχιση} other {# αντιστοιχίσεις}}",
+ "copy": "Αντιγραφή",
+ "copied": "Αντιγράφηκε"
+ },
+ "KeycodeInspector": {
+ "title": "Επιθεωρητής πλήκτρων",
+ "subtitle": "Πατήστε οποιοδήποτε πλήκτρο για να επιθεωρήσετε το συμβάν πληκτρολογίου JavaScript του",
+ "capture": {
+ "prompt": "Πατήστε οποιοδήποτε πλήκτρο…",
+ "ready": "Ακρόαση — πατήστε οποιοδήποτε πλήκτρο στο πληκτρολόγιό σας",
+ "clickToFocus": "Κάντε κλικ εδώ για εστίαση, μετά πατήστε οποιοδήποτε πλήκτρο"
+ },
+ "hero": {
+ "spaceKey": "(Διάστημα)",
+ "keyLabel": "event.key",
+ "repeat": "επανάληψη"
+ },
+ "copyJson": "Αντιγραφή JSON",
+ "copiedJson": "Το JSON του συμβάντος αντιγράφηκε στο πρόχειρο",
+ "deprecated": "καταργημένο",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Τυπική",
+ "left": "Αριστερά",
+ "right": "Δεξιά",
+ "numpad": "Αριθμητικό πληκτρολόγιο",
+ "unknown": "Άγνωστη"
+ },
+ "history": {
+ "title": "Ιστορικό",
+ "clear": "Καθαρισμός ιστορικού",
+ "empty": "Τα πατήματα πλήκτρων θα εμφανιστούν εδώ."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Γεννήτρια πινάκων Markdown",
+ "subtitle": "Επεξεργαστείτε ένα πλέγμα, επικολλήστε από υπολογιστικό φύλλο και αντιγράψτε έναν όμορφα στοιχισμένο πίνακα Markdown ή HTML",
+ "addRow": "Προσθήκη γραμμής",
+ "addColumn": "Προσθήκη στήλης",
+ "importLabel": "Εισαγωγή",
+ "importPlaceholder": "Επικολλήστε TSV (από Excel / Φύλλα Google) ή CSV εδώ…",
+ "importButton": "Εισαγωγή στο πλέγμα",
+ "pasteClipboard": "Επικόλληση από το πρόχειρο",
+ "importEmpty": "Δεν υπάρχει τίποτα για εισαγωγή — επικολλήστε πρώτα κάποιες γραμμές",
+ "clipboardError": "Δεν ήταν δυνατή η ανάγνωση από το πρόχειρο",
+ "clear": "Καθαρισμός",
+ "panels": {
+ "editor": "Επεξεργαστής πίνακα",
+ "output": "Έξοδος"
+ },
+ "align": {
+ "left": "Στοίχιση αριστερά",
+ "center": "Στοίχιση στο κέντρο",
+ "right": "Στοίχιση δεξιά"
+ },
+ "headerPlaceholder": "Κεφαλίδα",
+ "cellPlaceholder": "Κελί",
+ "removeRow": "Αφαίρεση γραμμής",
+ "removeColumn": "Αφαίρεση στήλης",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Αντιγραφή",
+ "copied": "Αντιγράφηκε"
+ },
+ "TimezoneConverter": {
+ "title": "Μετατροπέας ζωνών ώρας",
+ "subtitle": "Μετατρέψτε μια στιγμή σε όλο τον κόσμο για τον προγραμματισμό συναντήσεων.",
+ "dateTimeLabel": "Ημερομηνία και ώρα",
+ "now": "Τώρα",
+ "sourceZoneLabel": "Ζώνη ώρας προέλευσης",
+ "selectZone": "Επιλογή ζώνης",
+ "searchZone": "Αναζήτηση ζωνών ώρας...",
+ "noZone": "Δεν βρέθηκε ζώνη ώρας.",
+ "scrubLabel": "Μετατόπιση ώρας",
+ "reset": "Επαναφορά",
+ "targetsLabel": "Ζώνες προορισμού",
+ "addZone": "Προσθήκη ζώνης",
+ "remove": "Αφαίρεση",
+ "localBadge": "Τοπική",
+ "invalidDate": "Εισαγάγετε έγκυρη ημερομηνία και ώρα.",
+ "bands": {
+ "work": "Ώρες εργασίας (09:00–18:00)",
+ "edge": "Οριακές ώρες (νωρίς / βράδυ)",
+ "night": "Νύχτα"
+ }
+ },
+ "TokenCounter": {
+ "title": "Μετρητής token LLM",
+ "subtitle": "Μετρήστε token, οπτικοποιήστε τμήματα και εκτιμήστε το κόστος API.",
+ "inputLabel": "Κείμενο",
+ "sample": "Δείγμα",
+ "clear": "Καθαρισμός",
+ "placeholder": "Επικολλήστε κείμενο, κώδικα ή JSON για μέτρηση token...",
+ "encoding": "Κωδικοποίηση",
+ "loading": "Φόρτωση tokenizer...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / σειρά o)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Token",
+ "characters": "Χαρακτήρες",
+ "words": "Λέξεις",
+ "ratio": "Token/λέξη",
+ "ratioSub": "περίπου"
+ },
+ "claudeRow": "Claude (κατά προσέγγιση)",
+ "estimate": "εκτίμηση",
+ "viz": {
+ "title": "Οπτικοποίηση token",
+ "toggle": "Εμφάνιση",
+ "hint": "Εναλλάξτε για να χρωματίσετε κάθε token ως τμήμα.",
+ "empty": "Εισαγάγετε κείμενο για την οπτικοποίηση token.",
+ "truncated": "Εμφάνιση των πρώτων {limit} από {total} token."
+ },
+ "cost": {
+ "title": "Υπολογιστής κόστους",
+ "disclaimer": "επεξεργάσιμες προεπιλογές — επαληθεύστε τις τρέχουσες τιμές",
+ "model": "Μοντέλο",
+ "expectedOutput": "Αναμενόμενα token εξόδου",
+ "customInput": "Είσοδος $/MTok",
+ "customOutput": "Έξοδος $/MTok",
+ "inputCost": "Κόστος εισόδου",
+ "outputCost": "Κόστος εξόδου",
+ "total": "Σύνολο",
+ "per1000": "Ανά 1000 κλήσεις"
+ }
+ },
+ "WebhookTester": {
+ "title": "Δοκιμαστής webhook",
+ "subtitle": "Δημιουργήστε ένα προσωρινό URL και επιθεωρήστε ζωντανά τα εισερχόμενα αιτήματα webhook.",
+ "intro": {
+ "title": "Δημιουργήστε ένα URL webhook για να ξεκινήσετε την καταγραφή αιτημάτων",
+ "hint": "Θα λάβετε ένα μη προβλέψιμο URL που καταγράφει κάθε εισερχόμενο αίτημα. Λήγει μετά από 24 ώρες."
+ },
+ "createBtn": "Δημιουργία URL webhook",
+ "creating": "Δημιουργία…",
+ "urlLabel": "Το URL webhook σας",
+ "expiresIn": "Λήγει σε {countdown}",
+ "copyUrlBtn": "Αντιγραφή URL",
+ "copied": "Αντιγράφηκε",
+ "clearBtn": "Καθαρισμός",
+ "newBinBtn": "Νέο URL",
+ "requestsTitle": "Αιτήματα",
+ "selectPrompt": "Επιλέξτε ένα αίτημα για να επιθεωρήσετε τις λεπτομέρειές του.",
+ "expiredNotice": "Αυτό το URL webhook έχει λήξει. Δημιουργήστε ένα νέο για να συνεχίσετε τις δοκιμές.",
+ "base64Badge": "base64",
+ "truncatedBadge": "περικομμένο",
+ "empty": {
+ "title": "Αναμονή αιτημάτων…",
+ "hint": "Στείλτε ένα αίτημα στο URL σας και θα εμφανιστεί εδώ αμέσως. Δοκιμάστε αυτό:"
+ },
+ "detail": {
+ "receivedAt": "Ελήφθη",
+ "source": "IP προέλευσης",
+ "contentType": "Content-Type",
+ "query": "Παράμετροι ερωτήματος",
+ "headers": "Κεφαλίδες",
+ "body": "Σώμα",
+ "copyBody": "Αντιγραφή σώματος",
+ "emptyBody": "Χωρίς σώμα"
+ },
+ "errors": {
+ "createFailed": "Η δημιουργία του URL webhook απέτυχε. Δοκιμάστε ξανά.",
+ "clearFailed": "Ο καθαρισμός των αιτημάτων απέτυχε. Δοκιμάστε ξανά."
+ }
+ },
+ "WebsocketTester": {
+ "title": "Δοκιμαστής WebSocket",
+ "subtitle": "Συνδεθείτε σε ένα άκρο WebSocket και στείλτε, λάβετε και επιθεωρήστε μηνύματα ζωντανά",
+ "urlLabel": "URL άκρου",
+ "protocolsLabel": "Υποπρωτόκολλα",
+ "protocolsPlaceholder": "προαιρετικό, διαχωρισμένα με κόμμα",
+ "connect": "Σύνδεση",
+ "disconnect": "Αποσύνδεση",
+ "status": {
+ "disconnected": "Αποσυνδεδεμένο",
+ "connecting": "Σύνδεση",
+ "connected": "Συνδεδεμένο",
+ "error": "Σφάλμα"
+ },
+ "messages": {
+ "connecting": "Σύνδεση σε {url}…",
+ "connected": "Η σύνδεση δημιουργήθηκε",
+ "connectedWithProtocol": "Η σύνδεση δημιουργήθηκε (πρωτόκολλο: {protocol})",
+ "invalidUrl": "Μη έγκυρο URL — πρέπει να ξεκινά με ws:// ή wss://",
+ "connectFailed": "Το άνοιγμα της σύνδεσης απέτυχε",
+ "socketError": "Παρουσιάστηκε σφάλμα σύνδεσης",
+ "notConnected": "Μη συνδεδεμένο — συνδεθείτε πρώτα",
+ "binaryMessage": "Ελήφθη δυαδικό μήνυμα"
+ },
+ "log": {
+ "title": "Αρχείο μηνυμάτων",
+ "empty": "Δεν υπάρχουν ακόμη μηνύματα. Συνδεθείτε και ξεκινήστε την αποστολή.",
+ "copyAll": "Αντιγραφή όλων",
+ "download": "Λήψη ως .txt",
+ "clear": "Καθαρισμός αρχείου",
+ "prettyPrint": "Μορφοποίηση JSON",
+ "showRaw": "Εμφάνιση ακατέργαστου κειμένου"
+ },
+ "composer": {
+ "placeholder": "Πληκτρολογήστε ένα μήνυμα για αποστολή…",
+ "send": "Αποστολή",
+ "formatJson": "Μορφοποίηση ως JSON",
+ "invalidJson": "Το μήνυμα δεν είναι έγκυρο JSON",
+ "hint": "Cmd/Ctrl+Enter για αποστολή"
+ },
+ "ping": {
+ "label": "Αυτόματο ping",
+ "seconds": "δευτ",
+ "intervalTitle": "Διάστημα ping σε δευτερόλεπτα",
+ "payloadPlaceholder": "φορτίο ping"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Αναζήτηση Whois",
+ "subtitle": "Αναζητήστε στοιχεία καταχώρισης για οποιονδήποτε τομέα ή διεύθυνση IP μέσω RDAP",
+ "placeholder": "example.com ή 8.8.8.8",
+ "lookupBtn": "Αναζήτηση",
+ "looking": "Αναζήτηση…",
+ "refreshBtn": "Ανανέωση",
+ "tryLabel": "Δοκιμάστε:",
+ "copy": "Αντιγραφή",
+ "copied": "Αντιγράφηκε",
+ "notAvailable": "Μη διαθέσιμο",
+ "summary": {
+ "registrar": "Καταχωρητής",
+ "network": "Δίκτυο"
+ },
+ "dates": {
+ "created": "Δημιουργήθηκε",
+ "updated": "Ενημερώθηκε",
+ "expires": "Λήγει"
+ },
+ "sections": {
+ "status": "Κατάσταση",
+ "nameservers": "Διακομιστές ονομάτων",
+ "contacts": "Επαφές",
+ "rawRdap": "Ακατέργαστο RDAP"
+ },
+ "contacts": {
+ "roles": "Ρόλοι",
+ "name": "Όνομα",
+ "organization": "Οργανισμός"
+ },
+ "errors": {
+ "invalidTarget": "Εισαγάγετε ένα έγκυρο όνομα τομέα ή διεύθυνση IP",
+ "lookupFailed": "Η αναζήτηση απέτυχε (κατάσταση {status})"
+ }
}
}
diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index b8145c94..8101e4a2 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Chmod Calculator",
+ "codeScreenshot": "Code Screenshot",
+ "cssGenerators": "CSS Generators",
+ "exifViewer": "EXIF Viewer",
+ "faviconGenerator": "Favicon Generator",
+ "jsonDiff": "JSON Diff",
+ "jsonpathPlayground": "JSONPath Playground",
+ "keycodeInspector": "Keycode Inspector",
+ "markdownTableGenerator": "Markdown Table",
+ "timezoneConverter": "Timezone Converter",
+ "tokenCounter": "Token Counter",
+ "webhookTester": "Webhook Tester",
+ "websocketTester": "WebSocket Tester",
+ "whoisLookup": "Whois Lookup"
},
"Help": {
"title": "Help & documentation",
@@ -642,6 +657,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Bcrypt Generator & Checker",
+ "description": "Hash passwords with bcrypt and verify existing hashes."
+ },
+ "chmodCalculator": {
+ "title": "Chmod Calculator",
+ "description": "Convert Unix permissions between octal, symbolic, and checkboxes."
+ },
+ "codeScreenshot": {
+ "title": "Code Screenshot",
+ "description": "Turn source code into beautiful shareable images."
+ },
+ "cssGenerators": {
+ "title": "CSS Generators",
+ "description": "Box-shadow, border-radius, and fluid clamp() generators."
+ },
+ "exifViewer": {
+ "title": "EXIF Viewer & Remover",
+ "description": "Inspect and strip photo EXIF metadata locally."
+ },
+ "faviconGenerator": {
+ "title": "Favicon Generator",
+ "description": "Generate favicons, app icons and manifest snippets."
+ },
+ "jsonDiff": {
+ "title": "JSON Diff",
+ "description": "Semantic, structure-aware diff of two JSON documents."
+ },
+ "jsonpathPlayground": {
+ "title": "JSONPath Playground",
+ "description": "Query JSON with JSONPath or JMESPath and see live results."
+ },
+ "keycodeInspector": {
+ "title": "Keycode Inspector",
+ "description": "Press a key to see its JavaScript event codes and modifiers."
+ },
+ "markdownTableGenerator": {
+ "title": "Markdown Table Generator",
+ "description": "Build Markdown and HTML tables in a spreadsheet-like grid editor."
+ },
+ "timezoneConverter": {
+ "title": "Timezone Converter",
+ "description": "Convert a time across many zones with day-shift and working-hours cues."
+ },
+ "tokenCounter": {
+ "title": "LLM Token Counter",
+ "description": "Count LLM tokens, visualize chunks, and estimate API cost."
+ },
+ "webhookTester": {
+ "title": "Webhook Tester",
+ "description": "Capture and inspect incoming webhook requests in real time."
+ },
+ "websocketTester": {
+ "title": "WebSocket Tester",
+ "description": "Connect to WebSocket endpoints and exchange messages in real time."
+ },
+ "whoisLookup": {
+ "title": "Whois Lookup",
+ "description": "Look up domain and IP registration details via RDAP."
}
}
},
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Bcrypt Generator & Checker",
+ "subtitle": "Hash passwords with bcrypt and verify hashes — everything stays in your browser",
+ "tabs": {
+ "hash": "Hash",
+ "verify": "Verify"
+ },
+ "showPassword": "Show password",
+ "hidePassword": "Hide password",
+ "copy": "Copy",
+ "copied": "Copied",
+ "hash": {
+ "passwordLabel": "Password",
+ "passwordPlaceholder": "Enter a password to hash…",
+ "costLabel": "Cost factor",
+ "costHint": "Higher cost doubles the work per step. 10-12 is a common choice for production.",
+ "slowWarning": "Cost factors of 13 and above can take several seconds to compute.",
+ "generate": "Generate hash",
+ "generating": "Hashing…",
+ "resultLabel": "Bcrypt hash",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "Hash anatomy",
+ "algorithm": "algorithm",
+ "cost": "cost",
+ "salt": "salt (22 chars)",
+ "hash": "hash (31 chars)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Password",
+ "passwordPlaceholder": "Enter the password to check…",
+ "hashLabel": "Bcrypt hash",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Enter a password and a bcrypt hash to compare them.",
+ "invalidHash": "This does not look like a valid bcrypt hash.",
+ "checking": "Comparing…",
+ "match": "Match — the password produces this hash.",
+ "noMatch": "No match — the password does not produce this hash."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Chmod Calculator",
+ "subtitle": "Convert Unix file permissions between checkboxes, octal, and symbolic notation",
+ "grid": {
+ "title": "Permissions",
+ "read": "Read",
+ "write": "Write",
+ "execute": "Execute",
+ "owner": "Owner",
+ "group": "Group",
+ "others": "Others"
+ },
+ "special": {
+ "title": "Special bits",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "Octal",
+ "placeholder": "755 or 4755",
+ "invalid": "Enter 3 or 4 octal digits (0-7)."
+ },
+ "symbolic": {
+ "label": "Symbolic",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Enter 9 characters like rwxr-xr-x (s/S, t/T allowed)."
+ },
+ "output": {
+ "title": "Command",
+ "copy": "Copy",
+ "copied": "Copied"
+ },
+ "explain": {
+ "line": "{cls} can {perms}.",
+ "none": "{cls} has no permissions.",
+ "setuid": "Setuid: the file runs with its owner's privileges.",
+ "setgid": "Setgid: the file runs with its group's privileges (new files in a directory inherit its group).",
+ "sticky": "Sticky: only a file's owner can delete or rename it inside this directory."
+ },
+ "presets": {
+ "title": "Common presets",
+ "octal": "Octal",
+ "permissions": "Permissions",
+ "description": "Typical use",
+ "p644": "Regular files: owner writes, everyone reads.",
+ "p755": "Executables and directories: everyone can read and enter.",
+ "p600": "Private files: only the owner reads and writes.",
+ "p700": "Private executables and directories: owner only.",
+ "p664": "Group-writable files.",
+ "p775": "Group-writable directories and executables.",
+ "p400": "Read-only for the owner (e.g. SSH private keys)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Code Screenshot",
+ "subtitle": "Turn code into beautiful, shareable images",
+ "languageLabel": "Language",
+ "autoDetect": "Auto-detect",
+ "backgroundLabel": "Background",
+ "paddingLabel": "Padding",
+ "filenameLabel": "Filename",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "Window dots",
+ "themeLabel": "Window theme",
+ "themeDark": "Dark",
+ "themeLight": "Light",
+ "scaleLabel": "Export scale",
+ "copyImage": "Copy image",
+ "copied": "Copied",
+ "exportPng": "Export PNG",
+ "inputLabel": "Code",
+ "detected": "Detected: {language}",
+ "clear": "Clear",
+ "placeholder": "Paste your code here…",
+ "previewLabel": "Preview",
+ "emptyPreview": "Your code preview will appear here",
+ "exportFailed": "Failed to export image",
+ "copyUnsupported": "Copying images is not supported in this browser",
+ "copyFailed": "Failed to copy image",
+ "backgrounds": {
+ "ocean": "Ocean",
+ "sunset": "Sunset",
+ "candy": "Candy",
+ "forest": "Forest",
+ "midnight": "Midnight",
+ "gold": "Gold",
+ "aurora": "Aurora",
+ "slate": "Slate",
+ "transparent": "Transparent"
+ }
+ },
+ "CssGenerators": {
+ "title": "CSS Generators",
+ "subtitle": "Box-shadow, border-radius, and fluid clamp() — copy the CSS.",
+ "preview": "Preview",
+ "output": "CSS",
+ "copy": "Copy",
+ "copied": "Copied",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "Fluid clamp()"
+ },
+ "boxShadow": {
+ "presets": "Presets",
+ "layer": "Layer",
+ "addLayer": "Add layer",
+ "removeLayer": "Remove layer",
+ "x": "X",
+ "y": "Y",
+ "blur": "Blur",
+ "spread": "Spread",
+ "color": "Color",
+ "alpha": "Alpha",
+ "inset": "Inset",
+ "presetNames": {
+ "subtleCard": "Subtle card",
+ "elevated": "Elevated",
+ "hardOffset": "Hard offset",
+ "neonGlow": "Neon glow",
+ "inner": "Inner",
+ "layeredSmooth": "Layered smooth"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Mode",
+ "modes": {
+ "simple": "Per-corner",
+ "blob": "Organic blob"
+ },
+ "unit": "Unit",
+ "linkCorners": "Link corners",
+ "randomize": "Randomize",
+ "horizontal": "Horizontal radii",
+ "vertical": "Vertical radii",
+ "corners": {
+ "tl": "Top left",
+ "tr": "Top right",
+ "br": "Bottom right",
+ "bl": "Bottom left"
+ }
+ },
+ "clamp": {
+ "minFont": "Min font (px)",
+ "maxFont": "Max font (px)",
+ "minViewport": "Min viewport (px)",
+ "maxViewport": "Max viewport (px)",
+ "rootFont": "Root font (px)",
+ "viewportWidth": "Simulated viewport",
+ "computedSize": "Computed size",
+ "slope": "Slope",
+ "intercept": "Intercept",
+ "previewText": "The quick brown fox",
+ "invalid": "Enter valid, differing viewport bounds."
+ }
+ },
+ "ExifViewer": {
+ "title": "EXIF Viewer & Remover",
+ "subtitle": "Inspect photo metadata and strip it in your browser",
+ "dropTitle": "Drop an image here",
+ "dropHint": "or click to browse — your image never leaves your device",
+ "browse": "Choose image",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Reading metadata…",
+ "parseFailed": "Failed to read this image",
+ "clear": "Remove image",
+ "gpsWarning": "Contains GPS location",
+ "openMap": "View on OpenStreetMap",
+ "noMetadata": "No embedded metadata was found in this image.",
+ "facts": {
+ "name": "Name",
+ "type": "Type",
+ "dimensions": "Dimensions",
+ "size": "Size"
+ },
+ "groups": {
+ "camera": "Camera",
+ "exposure": "Exposure",
+ "timestamps": "Timestamps",
+ "gps": "GPS Location",
+ "software": "Software & Other"
+ },
+ "gpsCard": {
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "altitude": "Altitude"
+ },
+ "raw": {
+ "title": "All metadata",
+ "copyJson": "Copy JSON",
+ "copied": "Copied"
+ },
+ "remove": {
+ "title": "Remove metadata",
+ "jpegDescription": "Losslessly remove EXIF, XMP, IPTC and comments without re-compressing the image.",
+ "reencodeDescription": "Re-encode the image to strip embedded metadata. This re-compresses the pixels.",
+ "keepIcc": "Keep color profile (ICC)",
+ "stripButton": "Strip metadata",
+ "reencodeButton": "Re-encode & strip",
+ "unsupported": "Metadata removal is not supported for this file type.",
+ "failed": "Failed to remove metadata",
+ "before": "Before",
+ "after": "After",
+ "download": "Download cleaned image"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Favicon Generator",
+ "subtitle": "Create favicons and app icons from one image",
+ "dropTitle": "Drop an image here",
+ "dropHint": "PNG, JPEG or SVG — click to browse",
+ "browse": "Choose image",
+ "clear": "Remove image",
+ "backgroundLabel": "Background",
+ "transparent": "Transparent",
+ "solidColor": "Solid",
+ "cornerLabel": "Corner shape",
+ "cornerNone": "Square",
+ "cornerRounded": "Rounded",
+ "cornerCircle": "Circle",
+ "maskPaddingLabel": "Maskable padding",
+ "maskable": "maskable",
+ "previewLabel": "Preview",
+ "copy": "Copy",
+ "headSnippet": "HTML head snippet",
+ "downloadIco": "Download favicon.ico",
+ "downloadZip": "Download all (.zip)"
+ },
+ "JsonDiff": {
+ "title": "JSON Diff",
+ "subtitle": "Semantic, structure-aware comparison of two JSON documents — key order and formatting ignored",
+ "options": {
+ "ignoreArrayOrder": "Ignore array order"
+ },
+ "loadBothSamples": "Load samples",
+ "sample": "Sample",
+ "format": "Format",
+ "clear": "Clear",
+ "formatError": "Cannot format: input is not valid JSON",
+ "placeholder": "Paste or type JSON here…",
+ "panels": {
+ "left": "Original (left)",
+ "right": "Modified (right)",
+ "results": "Differences"
+ },
+ "summary": {
+ "added": "{count} added",
+ "removed": "{count} removed",
+ "changed": "{count} changed"
+ },
+ "types": {
+ "added": "Added",
+ "removed": "Removed",
+ "changed": "Changed"
+ },
+ "copyChanges": "Copy as JSON",
+ "copied": "Copied",
+ "empty": "Paste JSON into both panels to see their differences.",
+ "identical": "The two documents are semantically identical."
+ },
+ "JsonpathPlayground": {
+ "title": "JSONPath Playground",
+ "subtitle": "Query JSON documents with JSONPath or JMESPath and see live results",
+ "engineLabel": "Engine",
+ "queryLabel": "Query",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Cheatsheet",
+ "cheats": {
+ "jpAllTitles": "All book titles",
+ "jpAllAuthors": "Every author, anywhere in the document",
+ "jpFirstBook": "First book",
+ "jpLastBook": "Last book",
+ "jpSlice": "First two books (slice)",
+ "jpFilterPrice": "Books cheaper than 40",
+ "jpFilterInStock": "Titles of in-stock books",
+ "jpRecursive": "All values, recursively",
+ "jmAllTitles": "All book titles",
+ "jmFirstAuthor": "Author of the first book",
+ "jmLastBook": "Last book",
+ "jmSlice": "First two books (slice)",
+ "jmFilterPrice": "Titles of books cheaper than 40",
+ "jmCount": "Count in-stock books",
+ "jmProjection": "Reshape each book into a new object",
+ "jmSort": "Titles sorted by price"
+ },
+ "panels": {
+ "input": "JSON input",
+ "results": "Results"
+ },
+ "sample": "Sample",
+ "format": "Format",
+ "clear": "Clear",
+ "formatError": "Cannot format: input is not valid JSON",
+ "placeholder": "Paste or type JSON here…",
+ "outputPlaceholder": "Matches will appear here…",
+ "matchCount": "{count, plural, one {# match} other {# matches}}",
+ "copy": "Copy",
+ "copied": "Copied"
+ },
+ "KeycodeInspector": {
+ "title": "Keycode Inspector",
+ "subtitle": "Press any key to inspect its JavaScript keyboard event",
+ "capture": {
+ "prompt": "Press any key…",
+ "ready": "Listening — press any key on your keyboard",
+ "clickToFocus": "Click here to focus, then press any key"
+ },
+ "hero": {
+ "spaceKey": "(Space)",
+ "keyLabel": "event.key",
+ "repeat": "repeat"
+ },
+ "copyJson": "Copy JSON",
+ "copiedJson": "Event JSON copied to clipboard",
+ "deprecated": "deprecated",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Standard",
+ "left": "Left",
+ "right": "Right",
+ "numpad": "Numpad",
+ "unknown": "Unknown"
+ },
+ "history": {
+ "title": "History",
+ "clear": "Clear history",
+ "empty": "Key presses will appear here."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Markdown Table Generator",
+ "subtitle": "Edit a grid, paste from a spreadsheet, and copy a pretty-aligned Markdown or HTML table",
+ "addRow": "Add row",
+ "addColumn": "Add column",
+ "importLabel": "Import",
+ "importPlaceholder": "Paste TSV (from Excel / Google Sheets) or CSV here…",
+ "importButton": "Import into grid",
+ "pasteClipboard": "Paste from clipboard",
+ "importEmpty": "Nothing to import — paste some rows first",
+ "clipboardError": "Could not read from the clipboard",
+ "clear": "Clear",
+ "panels": {
+ "editor": "Table editor",
+ "output": "Output"
+ },
+ "align": {
+ "left": "Align left",
+ "center": "Align center",
+ "right": "Align right"
+ },
+ "headerPlaceholder": "Header",
+ "cellPlaceholder": "Cell",
+ "removeRow": "Remove row",
+ "removeColumn": "Remove column",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Copy",
+ "copied": "Copied"
+ },
+ "TimezoneConverter": {
+ "title": "Timezone Converter",
+ "subtitle": "Convert one moment across the world for meeting planning.",
+ "dateTimeLabel": "Date & time",
+ "now": "Now",
+ "sourceZoneLabel": "Source timezone",
+ "selectZone": "Select zone",
+ "searchZone": "Search timezones...",
+ "noZone": "No timezone found.",
+ "scrubLabel": "Shift time",
+ "reset": "Reset",
+ "targetsLabel": "Target zones",
+ "addZone": "Add zone",
+ "remove": "Remove",
+ "localBadge": "Local",
+ "invalidDate": "Enter a valid date and time.",
+ "bands": {
+ "work": "Working hours (09:00–18:00)",
+ "edge": "Edge hours (early / evening)",
+ "night": "Night"
+ }
+ },
+ "TokenCounter": {
+ "title": "LLM Token Counter",
+ "subtitle": "Count tokens, visualize chunks, and estimate API cost.",
+ "inputLabel": "Text",
+ "sample": "Sample",
+ "clear": "Clear",
+ "placeholder": "Paste text, code, or JSON to count tokens...",
+ "encoding": "Encoding",
+ "loading": "Loading tokenizer...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / o-series)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Tokens",
+ "characters": "Characters",
+ "words": "Words",
+ "ratio": "Tokens/word",
+ "ratioSub": "approx"
+ },
+ "claudeRow": "Claude (approximate)",
+ "estimate": "estimate",
+ "viz": {
+ "title": "Token visualization",
+ "toggle": "Show",
+ "hint": "Toggle to color each token as a chunk.",
+ "empty": "Enter text to visualize tokens.",
+ "truncated": "Showing the first {limit} of {total} tokens."
+ },
+ "cost": {
+ "title": "Cost calculator",
+ "disclaimer": "editable defaults — verify current pricing",
+ "model": "Model",
+ "expectedOutput": "Expected output tokens",
+ "customInput": "Input $/MTok",
+ "customOutput": "Output $/MTok",
+ "inputCost": "Input cost",
+ "outputCost": "Output cost",
+ "total": "Total",
+ "per1000": "Per 1000 calls"
+ }
+ },
+ "WebhookTester": {
+ "title": "Webhook Tester",
+ "subtitle": "Create a temporary URL and inspect incoming webhook requests live.",
+ "intro": {
+ "title": "Create a webhook URL to start capturing requests",
+ "hint": "You'll get an unguessable URL that records every incoming request. It expires after 24 hours."
+ },
+ "createBtn": "Create webhook URL",
+ "creating": "Creating…",
+ "urlLabel": "Your webhook URL",
+ "expiresIn": "Expires in {countdown}",
+ "copyUrlBtn": "Copy URL",
+ "copied": "Copied",
+ "clearBtn": "Clear",
+ "newBinBtn": "New URL",
+ "requestsTitle": "Requests",
+ "selectPrompt": "Select a request to inspect its details.",
+ "expiredNotice": "This webhook URL has expired. Create a new one to keep testing.",
+ "base64Badge": "base64",
+ "truncatedBadge": "truncated",
+ "empty": {
+ "title": "Waiting for requests…",
+ "hint": "Send a request to your URL and it will appear here instantly. Try this:"
+ },
+ "detail": {
+ "receivedAt": "Received",
+ "source": "Source IP",
+ "contentType": "Content-Type",
+ "query": "Query params",
+ "headers": "Headers",
+ "body": "Body",
+ "copyBody": "Copy body",
+ "emptyBody": "No body"
+ },
+ "errors": {
+ "createFailed": "Failed to create webhook URL. Please try again.",
+ "clearFailed": "Failed to clear requests. Please try again."
+ }
+ },
+ "WebsocketTester": {
+ "title": "WebSocket Tester",
+ "subtitle": "Connect to a WebSocket endpoint and send, receive, and inspect messages live",
+ "urlLabel": "Endpoint URL",
+ "protocolsLabel": "Subprotocols",
+ "protocolsPlaceholder": "optional, comma separated",
+ "connect": "Connect",
+ "disconnect": "Disconnect",
+ "status": {
+ "disconnected": "Disconnected",
+ "connecting": "Connecting",
+ "connected": "Connected",
+ "error": "Error"
+ },
+ "messages": {
+ "connecting": "Connecting to {url}…",
+ "connected": "Connection established",
+ "connectedWithProtocol": "Connection established (protocol: {protocol})",
+ "invalidUrl": "Invalid URL — must start with ws:// or wss://",
+ "connectFailed": "Failed to open connection",
+ "socketError": "A connection error occurred",
+ "notConnected": "Not connected — connect first",
+ "binaryMessage": "Received a binary message"
+ },
+ "log": {
+ "title": "Message Log",
+ "empty": "No messages yet. Connect and start sending.",
+ "copyAll": "Copy all",
+ "download": "Download as .txt",
+ "clear": "Clear log",
+ "prettyPrint": "Pretty-print JSON",
+ "showRaw": "Show raw text"
+ },
+ "composer": {
+ "placeholder": "Type a message to send…",
+ "send": "Send",
+ "formatJson": "Format as JSON",
+ "invalidJson": "Message is not valid JSON",
+ "hint": "Cmd/Ctrl+Enter to send"
+ },
+ "ping": {
+ "label": "Auto-ping",
+ "seconds": "sec",
+ "intervalTitle": "Ping interval in seconds",
+ "payloadPlaceholder": "ping payload"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Whois Lookup",
+ "subtitle": "Look up registration details for any domain or IP address via RDAP",
+ "placeholder": "example.com or 8.8.8.8",
+ "lookupBtn": "Lookup",
+ "looking": "Looking up…",
+ "refreshBtn": "Refresh",
+ "tryLabel": "Try:",
+ "copy": "Copy",
+ "copied": "Copied",
+ "notAvailable": "Not available",
+ "summary": {
+ "registrar": "Registrar",
+ "network": "Network"
+ },
+ "dates": {
+ "created": "Created",
+ "updated": "Updated",
+ "expires": "Expires"
+ },
+ "sections": {
+ "status": "Status",
+ "nameservers": "Nameservers",
+ "contacts": "Contacts",
+ "rawRdap": "Raw RDAP"
+ },
+ "contacts": {
+ "roles": "Roles",
+ "name": "Name",
+ "organization": "Organization"
+ },
+ "errors": {
+ "invalidTarget": "Enter a valid domain name or IP address",
+ "lookupFailed": "Lookup failed (status {status})"
+ }
}
}
diff --git a/apps/web/messages/es.json b/apps/web/messages/es.json
index d04dbd2f..fd0c8625 100644
--- a/apps/web/messages/es.json
+++ b/apps/web/messages/es.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Calculadora Chmod",
+ "codeScreenshot": "Captura de código",
+ "cssGenerators": "Generadores CSS",
+ "exifViewer": "Visor EXIF",
+ "faviconGenerator": "Generador de favicon",
+ "jsonDiff": "Diff JSON",
+ "jsonpathPlayground": "Playground JSONPath",
+ "keycodeInspector": "Inspector de keycode",
+ "markdownTableGenerator": "Tabla Markdown",
+ "timezoneConverter": "Conversor de zonas horarias",
+ "tokenCounter": "Contador de tokens",
+ "webhookTester": "Probador de webhooks",
+ "websocketTester": "Probador de WebSocket",
+ "whoisLookup": "Consulta Whois"
},
"Help": {
"title": "Ayuda y documentación",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Generador y verificador Bcrypt",
+ "description": "Genera hashes de contraseñas con bcrypt y verifica hashes existentes."
+ },
+ "chmodCalculator": {
+ "title": "Calculadora Chmod",
+ "description": "Convierte permisos de Unix entre octal, simbólico y casillas."
+ },
+ "codeScreenshot": {
+ "title": "Captura de código",
+ "description": "Convierte tu código fuente en imágenes atractivas y fáciles de compartir."
+ },
+ "cssGenerators": {
+ "title": "Generadores CSS",
+ "description": "Generadores de box-shadow, border-radius y clamp() fluido."
+ },
+ "exifViewer": {
+ "title": "Visor y eliminador EXIF",
+ "description": "Inspecciona y elimina los metadatos EXIF de las fotos de forma local."
+ },
+ "faviconGenerator": {
+ "title": "Generador de favicon",
+ "description": "Genera favicons, iconos de app y fragmentos de manifiesto."
+ },
+ "jsonDiff": {
+ "title": "Diff JSON",
+ "description": "Comparación semántica y estructural de dos documentos JSON."
+ },
+ "jsonpathPlayground": {
+ "title": "Playground JSONPath",
+ "description": "Consulta JSON con JSONPath o JMESPath y ve los resultados en vivo."
+ },
+ "keycodeInspector": {
+ "title": "Inspector de keycode",
+ "description": "Pulsa una tecla para ver sus códigos de evento de JavaScript y sus modificadores."
+ },
+ "markdownTableGenerator": {
+ "title": "Generador de tablas Markdown",
+ "description": "Crea tablas Markdown y HTML en un editor de cuadrícula tipo hoja de cálculo."
+ },
+ "timezoneConverter": {
+ "title": "Conversor de zonas horarias",
+ "description": "Convierte una hora entre muchas zonas con indicaciones de cambio de día y horario laboral."
+ },
+ "tokenCounter": {
+ "title": "Contador de tokens de LLM",
+ "description": "Cuenta tokens de LLM, visualiza fragmentos y estima el coste de la API."
+ },
+ "webhookTester": {
+ "title": "Probador de webhooks",
+ "description": "Captura e inspecciona solicitudes de webhook entrantes en tiempo real."
+ },
+ "websocketTester": {
+ "title": "Probador de WebSocket",
+ "description": "Conéctate a endpoints WebSocket e intercambia mensajes en tiempo real."
+ },
+ "whoisLookup": {
+ "title": "Consulta Whois",
+ "description": "Consulta los datos de registro de un dominio o IP mediante RDAP."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Generador y verificador Bcrypt",
+ "subtitle": "Genera hashes de contraseñas con bcrypt y verifica hashes: todo permanece en tu navegador",
+ "tabs": {
+ "hash": "Hashear",
+ "verify": "Verificar"
+ },
+ "showPassword": "Mostrar contraseña",
+ "hidePassword": "Ocultar contraseña",
+ "copy": "Copiar",
+ "copied": "Copiado",
+ "hash": {
+ "passwordLabel": "Contraseña",
+ "passwordPlaceholder": "Introduce una contraseña para hashear…",
+ "costLabel": "Factor de coste",
+ "costHint": "Un coste mayor duplica el trabajo por paso. 10-12 es una elección habitual en producción.",
+ "slowWarning": "Los factores de coste de 13 o más pueden tardar varios segundos en calcularse.",
+ "generate": "Generar hash",
+ "generating": "Hasheando…",
+ "resultLabel": "Hash bcrypt",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "Anatomía del hash",
+ "algorithm": "algoritmo",
+ "cost": "coste",
+ "salt": "salt (22 caracteres)",
+ "hash": "hash (31 caracteres)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Contraseña",
+ "passwordPlaceholder": "Introduce la contraseña a comprobar…",
+ "hashLabel": "Hash bcrypt",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Introduce una contraseña y un hash bcrypt para compararlos.",
+ "invalidHash": "Esto no parece un hash bcrypt válido.",
+ "checking": "Comparando…",
+ "match": "Coincide: la contraseña produce este hash.",
+ "noMatch": "No coincide: la contraseña no produce este hash."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Calculadora Chmod",
+ "subtitle": "Convierte permisos de archivos Unix entre casillas, octal y notación simbólica",
+ "grid": {
+ "title": "Permisos",
+ "read": "Lectura",
+ "write": "Escritura",
+ "execute": "Ejecución",
+ "owner": "Propietario",
+ "group": "Grupo",
+ "others": "Otros"
+ },
+ "special": {
+ "title": "Bits especiales",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "Octal",
+ "placeholder": "755 o 4755",
+ "invalid": "Introduce 3 o 4 dígitos octales (0-7)."
+ },
+ "symbolic": {
+ "label": "Simbólico",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Introduce 9 caracteres como rwxr-xr-x (se permiten s/S, t/T)."
+ },
+ "output": {
+ "title": "Comando",
+ "copy": "Copiar",
+ "copied": "Copiado"
+ },
+ "explain": {
+ "line": "{cls} puede {perms}.",
+ "none": "{cls} no tiene permisos.",
+ "setuid": "Setuid: el archivo se ejecuta con los privilegios de su propietario.",
+ "setgid": "Setgid: el archivo se ejecuta con los privilegios de su grupo (los archivos nuevos de un directorio heredan su grupo).",
+ "sticky": "Sticky: solo el propietario de un archivo puede eliminarlo o renombrarlo dentro de este directorio."
+ },
+ "presets": {
+ "title": "Ajustes habituales",
+ "octal": "Octal",
+ "permissions": "Permisos",
+ "description": "Uso típico",
+ "p644": "Archivos normales: el propietario escribe, todos leen.",
+ "p755": "Ejecutables y directorios: todos pueden leer y entrar.",
+ "p600": "Archivos privados: solo el propietario lee y escribe.",
+ "p700": "Ejecutables y directorios privados: solo el propietario.",
+ "p664": "Archivos con escritura para el grupo.",
+ "p775": "Directorios y ejecutables con escritura para el grupo.",
+ "p400": "Solo lectura para el propietario (p. ej. claves privadas SSH)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Captura de código",
+ "subtitle": "Convierte código en imágenes atractivas y fáciles de compartir",
+ "languageLabel": "Lenguaje",
+ "autoDetect": "Detección automática",
+ "backgroundLabel": "Fondo",
+ "paddingLabel": "Relleno",
+ "filenameLabel": "Nombre de archivo",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "Puntos de ventana",
+ "themeLabel": "Tema de ventana",
+ "themeDark": "Oscuro",
+ "themeLight": "Claro",
+ "scaleLabel": "Escala de exportación",
+ "copyImage": "Copiar imagen",
+ "copied": "Copiado",
+ "exportPng": "Exportar PNG",
+ "inputLabel": "Código",
+ "detected": "Detectado: {language}",
+ "clear": "Limpiar",
+ "placeholder": "Pega tu código aquí…",
+ "previewLabel": "Vista previa",
+ "emptyPreview": "La vista previa de tu código aparecerá aquí",
+ "exportFailed": "No se pudo exportar la imagen",
+ "copyUnsupported": "Copiar imágenes no es compatible con este navegador",
+ "copyFailed": "No se pudo copiar la imagen",
+ "backgrounds": {
+ "ocean": "Océano",
+ "sunset": "Atardecer",
+ "candy": "Caramelo",
+ "forest": "Bosque",
+ "midnight": "Medianoche",
+ "gold": "Oro",
+ "aurora": "Aurora",
+ "slate": "Pizarra",
+ "transparent": "Transparente"
+ }
+ },
+ "CssGenerators": {
+ "title": "Generadores CSS",
+ "subtitle": "Box-shadow, border-radius y clamp() fluido: copia el CSS.",
+ "preview": "Vista previa",
+ "output": "CSS",
+ "copy": "Copiar",
+ "copied": "Copiado",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "clamp() fluido"
+ },
+ "boxShadow": {
+ "presets": "Ajustes",
+ "layer": "Capa",
+ "addLayer": "Añadir capa",
+ "removeLayer": "Quitar capa",
+ "x": "X",
+ "y": "Y",
+ "blur": "Desenfoque",
+ "spread": "Extensión",
+ "color": "Color",
+ "alpha": "Alfa",
+ "inset": "Interior",
+ "presetNames": {
+ "subtleCard": "Tarjeta sutil",
+ "elevated": "Elevado",
+ "hardOffset": "Desplazamiento marcado",
+ "neonGlow": "Brillo neón",
+ "inner": "Interior",
+ "layeredSmooth": "Capas suaves"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Modo",
+ "modes": {
+ "simple": "Por esquina",
+ "blob": "Blob orgánico"
+ },
+ "unit": "Unidad",
+ "linkCorners": "Vincular esquinas",
+ "randomize": "Aleatorizar",
+ "horizontal": "Radios horizontales",
+ "vertical": "Radios verticales",
+ "corners": {
+ "tl": "Superior izquierda",
+ "tr": "Superior derecha",
+ "br": "Inferior derecha",
+ "bl": "Inferior izquierda"
+ }
+ },
+ "clamp": {
+ "minFont": "Fuente mín. (px)",
+ "maxFont": "Fuente máx. (px)",
+ "minViewport": "Viewport mín. (px)",
+ "maxViewport": "Viewport máx. (px)",
+ "rootFont": "Fuente raíz (px)",
+ "viewportWidth": "Viewport simulado",
+ "computedSize": "Tamaño calculado",
+ "slope": "Pendiente",
+ "intercept": "Intersección",
+ "previewText": "El veloz murciélago hindú",
+ "invalid": "Introduce límites de viewport válidos y distintos."
+ }
+ },
+ "ExifViewer": {
+ "title": "Visor y eliminador EXIF",
+ "subtitle": "Inspecciona los metadatos de las fotos y elimínalos en tu navegador",
+ "dropTitle": "Suelta una imagen aquí",
+ "dropHint": "o haz clic para buscar: tu imagen nunca sale de tu dispositivo",
+ "browse": "Elegir imagen",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Leyendo metadatos…",
+ "parseFailed": "No se pudo leer esta imagen",
+ "clear": "Quitar imagen",
+ "gpsWarning": "Contiene ubicación GPS",
+ "openMap": "Ver en OpenStreetMap",
+ "noMetadata": "No se encontraron metadatos incrustados en esta imagen.",
+ "facts": {
+ "name": "Nombre",
+ "type": "Tipo",
+ "dimensions": "Dimensiones",
+ "size": "Tamaño"
+ },
+ "groups": {
+ "camera": "Cámara",
+ "exposure": "Exposición",
+ "timestamps": "Marcas de tiempo",
+ "gps": "Ubicación GPS",
+ "software": "Software y otros"
+ },
+ "gpsCard": {
+ "latitude": "Latitud",
+ "longitude": "Longitud",
+ "altitude": "Altitud"
+ },
+ "raw": {
+ "title": "Todos los metadatos",
+ "copyJson": "Copiar JSON",
+ "copied": "Copiado"
+ },
+ "remove": {
+ "title": "Eliminar metadatos",
+ "jpegDescription": "Elimina sin pérdidas EXIF, XMP, IPTC y comentarios sin recomprimir la imagen.",
+ "reencodeDescription": "Recodifica la imagen para eliminar los metadatos incrustados. Esto recomprime los píxeles.",
+ "keepIcc": "Conservar el perfil de color (ICC)",
+ "stripButton": "Eliminar metadatos",
+ "reencodeButton": "Recodificar y limpiar",
+ "unsupported": "La eliminación de metadatos no es compatible con este tipo de archivo.",
+ "failed": "No se pudieron eliminar los metadatos",
+ "before": "Antes",
+ "after": "Después",
+ "download": "Descargar imagen limpia"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Generador de favicon",
+ "subtitle": "Crea favicons e iconos de app a partir de una sola imagen",
+ "dropTitle": "Suelta una imagen aquí",
+ "dropHint": "PNG, JPEG o SVG: haz clic para buscar",
+ "browse": "Elegir imagen",
+ "clear": "Quitar imagen",
+ "backgroundLabel": "Fondo",
+ "transparent": "Transparente",
+ "solidColor": "Sólido",
+ "cornerLabel": "Forma de las esquinas",
+ "cornerNone": "Cuadrado",
+ "cornerRounded": "Redondeado",
+ "cornerCircle": "Círculo",
+ "maskPaddingLabel": "Relleno maskable",
+ "maskable": "maskable",
+ "previewLabel": "Vista previa",
+ "copy": "Copiar",
+ "headSnippet": "Fragmento para el head HTML",
+ "downloadIco": "Descargar favicon.ico",
+ "downloadZip": "Descargar todo (.zip)"
+ },
+ "JsonDiff": {
+ "title": "Diff JSON",
+ "subtitle": "Comparación semántica y estructural de dos documentos JSON: se ignoran el orden de las claves y el formato",
+ "options": {
+ "ignoreArrayOrder": "Ignorar el orden de los arrays"
+ },
+ "loadBothSamples": "Cargar ejemplos",
+ "sample": "Cargar ejemplo",
+ "format": "Formatear",
+ "clear": "Limpiar",
+ "formatError": "No se puede formatear: la entrada no es un JSON válido",
+ "placeholder": "Pega o escribe JSON aquí…",
+ "panels": {
+ "left": "Original (izquierda)",
+ "right": "Modificado (derecha)",
+ "results": "Diferencias"
+ },
+ "summary": {
+ "added": "{count} añadidos",
+ "removed": "{count} eliminados",
+ "changed": "{count} modificados"
+ },
+ "types": {
+ "added": "Añadido",
+ "removed": "Eliminado",
+ "changed": "Modificado"
+ },
+ "copyChanges": "Copiar como JSON",
+ "copied": "Copiado",
+ "empty": "Pega JSON en ambos paneles para ver sus diferencias.",
+ "identical": "Los dos documentos son semánticamente idénticos."
+ },
+ "JsonpathPlayground": {
+ "title": "Playground JSONPath",
+ "subtitle": "Consulta documentos JSON con JSONPath o JMESPath y ve los resultados en vivo",
+ "engineLabel": "Motor",
+ "queryLabel": "Consulta",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Chuleta",
+ "cheats": {
+ "jpAllTitles": "Todos los títulos de libros",
+ "jpAllAuthors": "Cada autor, en cualquier parte del documento",
+ "jpFirstBook": "Primer libro",
+ "jpLastBook": "Último libro",
+ "jpSlice": "Los dos primeros libros (slice)",
+ "jpFilterPrice": "Libros de menos de 40",
+ "jpFilterInStock": "Títulos de libros en stock",
+ "jpRecursive": "Todos los valores, de forma recursiva",
+ "jmAllTitles": "Todos los títulos de libros",
+ "jmFirstAuthor": "Autor del primer libro",
+ "jmLastBook": "Último libro",
+ "jmSlice": "Los dos primeros libros (slice)",
+ "jmFilterPrice": "Títulos de libros de menos de 40",
+ "jmCount": "Contar libros en stock",
+ "jmProjection": "Reformar cada libro en un nuevo objeto",
+ "jmSort": "Títulos ordenados por precio"
+ },
+ "panels": {
+ "input": "Entrada JSON",
+ "results": "Resultados"
+ },
+ "sample": "Cargar ejemplo",
+ "format": "Formatear",
+ "clear": "Limpiar",
+ "formatError": "No se puede formatear: la entrada no es un JSON válido",
+ "placeholder": "Pega o escribe JSON aquí…",
+ "outputPlaceholder": "Las coincidencias aparecerán aquí…",
+ "matchCount": "{count, plural, one {# coincidencia} other {# coincidencias}}",
+ "copy": "Copiar",
+ "copied": "Copiado"
+ },
+ "KeycodeInspector": {
+ "title": "Inspector de keycode",
+ "subtitle": "Pulsa cualquier tecla para inspeccionar su evento de teclado de JavaScript",
+ "capture": {
+ "prompt": "Pulsa cualquier tecla…",
+ "ready": "Escuchando: pulsa cualquier tecla de tu teclado",
+ "clickToFocus": "Haz clic aquí para enfocar y luego pulsa cualquier tecla"
+ },
+ "hero": {
+ "spaceKey": "(Espacio)",
+ "keyLabel": "event.key",
+ "repeat": "repetición"
+ },
+ "copyJson": "Copiar JSON",
+ "copiedJson": "JSON del evento copiado al portapapeles",
+ "deprecated": "obsoleto",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Estándar",
+ "left": "Izquierda",
+ "right": "Derecha",
+ "numpad": "Teclado numérico",
+ "unknown": "Desconocido"
+ },
+ "history": {
+ "title": "Historial",
+ "clear": "Borrar historial",
+ "empty": "Las pulsaciones de teclas aparecerán aquí."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Generador de tablas Markdown",
+ "subtitle": "Edita una cuadrícula, pega desde una hoja de cálculo y copia una tabla Markdown o HTML bien alineada",
+ "addRow": "Añadir fila",
+ "addColumn": "Añadir columna",
+ "importLabel": "Importar",
+ "importPlaceholder": "Pega TSV (desde Excel / Google Sheets) o CSV aquí…",
+ "importButton": "Importar a la cuadrícula",
+ "pasteClipboard": "Pegar desde el portapapeles",
+ "importEmpty": "Nada que importar: pega primero algunas filas",
+ "clipboardError": "No se pudo leer el portapapeles",
+ "clear": "Limpiar",
+ "panels": {
+ "editor": "Editor de tabla",
+ "output": "Salida"
+ },
+ "align": {
+ "left": "Alinear a la izquierda",
+ "center": "Centrar",
+ "right": "Alinear a la derecha"
+ },
+ "headerPlaceholder": "Encabezado",
+ "cellPlaceholder": "Celda",
+ "removeRow": "Quitar fila",
+ "removeColumn": "Quitar columna",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Copiar",
+ "copied": "Copiado"
+ },
+ "TimezoneConverter": {
+ "title": "Conversor de zonas horarias",
+ "subtitle": "Convierte un mismo momento por todo el mundo para planificar reuniones.",
+ "dateTimeLabel": "Fecha y hora",
+ "now": "Ahora",
+ "sourceZoneLabel": "Zona horaria de origen",
+ "selectZone": "Seleccionar zona",
+ "searchZone": "Buscar zonas horarias...",
+ "noZone": "No se encontró ninguna zona horaria.",
+ "scrubLabel": "Desplazar la hora",
+ "reset": "Restablecer",
+ "targetsLabel": "Zonas de destino",
+ "addZone": "Añadir zona",
+ "remove": "Quitar",
+ "localBadge": "Local",
+ "invalidDate": "Introduce una fecha y hora válidas.",
+ "bands": {
+ "work": "Horario laboral (09:00–18:00)",
+ "edge": "Horas límite (temprano / noche)",
+ "night": "Noche"
+ }
+ },
+ "TokenCounter": {
+ "title": "Contador de tokens de LLM",
+ "subtitle": "Cuenta tokens, visualiza fragmentos y estima el coste de la API.",
+ "inputLabel": "Texto",
+ "sample": "Cargar ejemplo",
+ "clear": "Limpiar",
+ "placeholder": "Pega texto, código o JSON para contar tokens...",
+ "encoding": "Codificación",
+ "loading": "Cargando el tokenizador...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / serie o)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Tokens",
+ "characters": "Caracteres",
+ "words": "Palabras",
+ "ratio": "Tokens/palabra",
+ "ratioSub": "aprox."
+ },
+ "claudeRow": "Claude (aproximado)",
+ "estimate": "estimación",
+ "viz": {
+ "title": "Visualización de tokens",
+ "toggle": "Mostrar",
+ "hint": "Activa para colorear cada token como un fragmento.",
+ "empty": "Introduce texto para visualizar los tokens.",
+ "truncated": "Mostrando los primeros {limit} de {total} tokens."
+ },
+ "cost": {
+ "title": "Calculadora de costes",
+ "disclaimer": "valores por defecto editables: verifica los precios actuales",
+ "model": "Modelo",
+ "expectedOutput": "Tokens de salida previstos",
+ "customInput": "Entrada $/MTok",
+ "customOutput": "Salida $/MTok",
+ "inputCost": "Coste de entrada",
+ "outputCost": "Coste de salida",
+ "total": "Total",
+ "per1000": "Por cada 1000 llamadas"
+ }
+ },
+ "WebhookTester": {
+ "title": "Probador de webhooks",
+ "subtitle": "Crea una URL temporal e inspecciona las solicitudes de webhook entrantes en vivo.",
+ "intro": {
+ "title": "Crea una URL de webhook para empezar a capturar solicitudes",
+ "hint": "Obtendrás una URL imposible de adivinar que registra cada solicitud entrante. Caduca a las 24 horas."
+ },
+ "createBtn": "Crear URL de webhook",
+ "creating": "Creando…",
+ "urlLabel": "Tu URL de webhook",
+ "expiresIn": "Caduca en {countdown}",
+ "copyUrlBtn": "Copiar URL",
+ "copied": "Copiado",
+ "clearBtn": "Limpiar",
+ "newBinBtn": "Nueva URL",
+ "requestsTitle": "Solicitudes",
+ "selectPrompt": "Selecciona una solicitud para inspeccionar sus detalles.",
+ "expiredNotice": "Esta URL de webhook ha caducado. Crea una nueva para seguir probando.",
+ "base64Badge": "base64",
+ "truncatedBadge": "truncado",
+ "empty": {
+ "title": "Esperando solicitudes…",
+ "hint": "Envía una solicitud a tu URL y aparecerá aquí al instante. Prueba esto:"
+ },
+ "detail": {
+ "receivedAt": "Recibido",
+ "source": "IP de origen",
+ "contentType": "Content-Type",
+ "query": "Parámetros de consulta",
+ "headers": "Cabeceras",
+ "body": "Cuerpo",
+ "copyBody": "Copiar cuerpo",
+ "emptyBody": "Sin cuerpo"
+ },
+ "errors": {
+ "createFailed": "No se pudo crear la URL de webhook. Inténtalo de nuevo.",
+ "clearFailed": "No se pudieron borrar las solicitudes. Inténtalo de nuevo."
+ }
+ },
+ "WebsocketTester": {
+ "title": "Probador de WebSocket",
+ "subtitle": "Conéctate a un endpoint WebSocket y envía, recibe e inspecciona mensajes en vivo",
+ "urlLabel": "URL del endpoint",
+ "protocolsLabel": "Subprotocolos",
+ "protocolsPlaceholder": "opcional, separados por comas",
+ "connect": "Conectar",
+ "disconnect": "Desconectar",
+ "status": {
+ "disconnected": "Desconectado",
+ "connecting": "Conectando",
+ "connected": "Conectado",
+ "error": "Error"
+ },
+ "messages": {
+ "connecting": "Conectando a {url}…",
+ "connected": "Conexión establecida",
+ "connectedWithProtocol": "Conexión establecida (protocolo: {protocol})",
+ "invalidUrl": "URL no válida: debe empezar por ws:// o wss://",
+ "connectFailed": "No se pudo abrir la conexión",
+ "socketError": "Se produjo un error de conexión",
+ "notConnected": "No conectado: conéctate primero",
+ "binaryMessage": "Se recibió un mensaje binario"
+ },
+ "log": {
+ "title": "Registro de mensajes",
+ "empty": "Todavía no hay mensajes. Conéctate y empieza a enviar.",
+ "copyAll": "Copiar todo",
+ "download": "Descargar como .txt",
+ "clear": "Borrar registro",
+ "prettyPrint": "Formatear JSON",
+ "showRaw": "Mostrar texto sin formato"
+ },
+ "composer": {
+ "placeholder": "Escribe un mensaje para enviar…",
+ "send": "Enviar",
+ "formatJson": "Formatear como JSON",
+ "invalidJson": "El mensaje no es un JSON válido",
+ "hint": "Cmd/Ctrl+Intro para enviar"
+ },
+ "ping": {
+ "label": "Auto-ping",
+ "seconds": "s",
+ "intervalTitle": "Intervalo de ping en segundos",
+ "payloadPlaceholder": "carga útil del ping"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Consulta Whois",
+ "subtitle": "Consulta los datos de registro de cualquier dominio o dirección IP mediante RDAP",
+ "placeholder": "example.com o 8.8.8.8",
+ "lookupBtn": "Consultar",
+ "looking": "Consultando…",
+ "refreshBtn": "Actualizar",
+ "tryLabel": "Prueba:",
+ "copy": "Copiar",
+ "copied": "Copiado",
+ "notAvailable": "No disponible",
+ "summary": {
+ "registrar": "Registrador",
+ "network": "Red"
+ },
+ "dates": {
+ "created": "Creado",
+ "updated": "Actualizado",
+ "expires": "Caduca"
+ },
+ "sections": {
+ "status": "Estado",
+ "nameservers": "Servidores de nombres",
+ "contacts": "Contactos",
+ "rawRdap": "RDAP sin procesar"
+ },
+ "contacts": {
+ "roles": "Roles",
+ "name": "Nombre",
+ "organization": "Organización"
+ },
+ "errors": {
+ "invalidTarget": "Introduce un nombre de dominio o dirección IP válidos",
+ "lookupFailed": "La consulta falló (estado {status})"
+ }
}
}
diff --git a/apps/web/messages/fa.json b/apps/web/messages/fa.json
index 832a3f4f..e68f40e2 100644
--- a/apps/web/messages/fa.json
+++ b/apps/web/messages/fa.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "ماشینحساب Chmod",
+ "codeScreenshot": "اسکرینشات کد",
+ "cssGenerators": "مولدهای CSS",
+ "exifViewer": "نمایشگر EXIF",
+ "faviconGenerator": "مولد Favicon",
+ "jsonDiff": "تفاوت JSON",
+ "jsonpathPlayground": "زمین بازی JSONPath",
+ "keycodeInspector": "بازرس کدِ کلید",
+ "markdownTableGenerator": "جدول Markdown",
+ "timezoneConverter": "مبدل منطقه زمانی",
+ "tokenCounter": "شمارنده توکن",
+ "webhookTester": "آزمایشگر Webhook",
+ "websocketTester": "آزمایشگر WebSocket",
+ "whoisLookup": "جستوجوی Whois"
},
"Help": {
"title": "راهنما و مستندات",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "مولد و بررسیکننده Bcrypt",
+ "description": "گذرواژهها را با bcrypt هش کنید و هشهای موجود را تأیید کنید."
+ },
+ "chmodCalculator": {
+ "title": "ماشینحساب Chmod",
+ "description": "مجوزهای یونیکس را بین هشتهشتی، نمادین و چکباکس تبدیل کنید."
+ },
+ "codeScreenshot": {
+ "title": "اسکرینشات کد",
+ "description": "کد منبع را به تصاویر زیبا و قابلاشتراک تبدیل کنید."
+ },
+ "cssGenerators": {
+ "title": "مولدهای CSS",
+ "description": "مولدهای box-shadow، border-radius و clamp() سیّال."
+ },
+ "exifViewer": {
+ "title": "نمایشگر و حذفکننده EXIF",
+ "description": "متادیتای EXIF عکس را بهصورت محلی بررسی و حذف کنید."
+ },
+ "faviconGenerator": {
+ "title": "مولد Favicon",
+ "description": "favicon، آیکونهای اپ و قطعهکدهای manifest بسازید."
+ },
+ "jsonDiff": {
+ "title": "تفاوت JSON",
+ "description": "مقایسه معنایی و ساختارآگاه دو سند JSON."
+ },
+ "jsonpathPlayground": {
+ "title": "زمین بازی JSONPath",
+ "description": "JSON را با JSONPath یا JMESPath پرسوجو کنید و نتایج زنده را ببینید."
+ },
+ "keycodeInspector": {
+ "title": "بازرس کدِ کلید",
+ "description": "کلیدی را فشار دهید تا کدهای رویداد JavaScript و کلیدهای تغییردهنده آن را ببینید."
+ },
+ "markdownTableGenerator": {
+ "title": "مولد جدول Markdown",
+ "description": "جدولهای Markdown و HTML را در ویرایشگر شبکهای شبیه صفحهگسترده بسازید."
+ },
+ "timezoneConverter": {
+ "title": "مبدل منطقه زمانی",
+ "description": "زمانی را در چند منطقه با نشانههای تغییر روز و ساعات کاری تبدیل کنید."
+ },
+ "tokenCounter": {
+ "title": "شمارنده توکن LLM",
+ "description": "توکنهای LLM را بشمارید، تکهها را تجسم کنید و هزینه API را تخمین بزنید."
+ },
+ "webhookTester": {
+ "title": "آزمایشگر Webhook",
+ "description": "درخواستهای ورودی webhook را در لحظه ثبت و بررسی کنید."
+ },
+ "websocketTester": {
+ "title": "آزمایشگر WebSocket",
+ "description": "به نقاط پایانی WebSocket متصل شوید و پیامها را در لحظه رد و بدل کنید."
+ },
+ "whoisLookup": {
+ "title": "جستوجوی Whois",
+ "description": "جزئیات ثبت دامنه و IP را از طریق RDAP جستوجو کنید."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "مولد و بررسیکننده Bcrypt",
+ "subtitle": "گذرواژهها را با bcrypt هش کنید و هشها را تأیید کنید — همهچیز در مرورگر شما میماند",
+ "tabs": {
+ "hash": "هش",
+ "verify": "تأیید"
+ },
+ "showPassword": "نمایش گذرواژه",
+ "hidePassword": "پنهانکردن گذرواژه",
+ "copy": "کپی",
+ "copied": "کپی شد",
+ "hash": {
+ "passwordLabel": "گذرواژه",
+ "passwordPlaceholder": "گذرواژهای برای هشکردن وارد کنید…",
+ "costLabel": "ضریب هزینه",
+ "costHint": "هزینه بالاتر کار هر مرحله را دو برابر میکند. ۱۰ تا ۱۲ انتخابی رایج برای محیط تولید است.",
+ "slowWarning": "ضریب هزینه ۱۳ به بالا ممکن است چند ثانیه برای محاسبه طول بکشد.",
+ "generate": "تولید هش",
+ "generating": "در حال هشکردن…",
+ "resultLabel": "هش Bcrypt",
+ "timeTaken": "{ms} میلیثانیه",
+ "anatomy": {
+ "title": "ساختار هش",
+ "algorithm": "الگوریتم",
+ "cost": "هزینه",
+ "salt": "salt (۲۲ کاراکتر)",
+ "hash": "هش (۳۱ کاراکتر)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "گذرواژه",
+ "passwordPlaceholder": "گذرواژهای برای بررسی وارد کنید…",
+ "hashLabel": "هش Bcrypt",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "یک گذرواژه و یک هش bcrypt وارد کنید تا مقایسه شوند.",
+ "invalidHash": "این بهنظر یک هش bcrypt معتبر نمیآید.",
+ "checking": "در حال مقایسه…",
+ "match": "منطبق است — گذرواژه این هش را تولید میکند.",
+ "noMatch": "منطبق نیست — گذرواژه این هش را تولید نمیکند."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "ماشینحساب Chmod",
+ "subtitle": "مجوزهای فایل یونیکس را بین چکباکس، هشتهشتی و نماد نمادین تبدیل کنید",
+ "grid": {
+ "title": "مجوزها",
+ "read": "خواندن",
+ "write": "نوشتن",
+ "execute": "اجرا",
+ "owner": "مالک",
+ "group": "گروه",
+ "others": "دیگران"
+ },
+ "special": {
+ "title": "بیتهای ویژه",
+ "setuid": "setuid",
+ "setgid": "setgid",
+ "sticky": "sticky"
+ },
+ "octal": {
+ "label": "هشتهشتی",
+ "placeholder": "755 یا 4755",
+ "invalid": "۳ یا ۴ رقم هشتهشتی (۰-۷) وارد کنید."
+ },
+ "symbolic": {
+ "label": "نمادین",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "۹ کاراکتر مانند rwxr-xr-x وارد کنید (s/S و t/T مجاز است)."
+ },
+ "output": {
+ "title": "دستور",
+ "copy": "کپی",
+ "copied": "کپی شد"
+ },
+ "explain": {
+ "line": "{cls} میتواند {perms}.",
+ "none": "{cls} هیچ مجوزی ندارد.",
+ "setuid": "setuid: فایل با امتیازات مالک خود اجرا میشود.",
+ "setgid": "setgid: فایل با امتیازات گروه خود اجرا میشود (فایلهای جدید در یک دایرکتوری گروه آن را به ارث میبرند).",
+ "sticky": "sticky: فقط مالک فایل میتواند آن را در این دایرکتوری حذف یا تغییرنام دهد."
+ },
+ "presets": {
+ "title": "پیشتنظیمهای رایج",
+ "octal": "هشتهشتی",
+ "permissions": "مجوزها",
+ "description": "کاربرد معمول",
+ "p644": "فایلهای معمولی: مالک مینویسد، همه میخوانند.",
+ "p755": "فایلهای اجرایی و دایرکتوریها: همه میتوانند بخوانند و وارد شوند.",
+ "p600": "فایلهای خصوصی: فقط مالک میخواند و مینویسد.",
+ "p700": "فایلهای اجرایی و دایرکتوریهای خصوصی: فقط مالک.",
+ "p664": "فایلهای قابلنوشتن توسط گروه.",
+ "p775": "دایرکتوریها و فایلهای اجرایی قابلنوشتن توسط گروه.",
+ "p400": "فقطخواندنی برای مالک (مثلاً کلیدهای خصوصی SSH)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "اسکرینشات کد",
+ "subtitle": "کد را به تصاویر زیبا و قابلاشتراک تبدیل کنید",
+ "languageLabel": "زبان",
+ "autoDetect": "تشخیص خودکار",
+ "backgroundLabel": "پسزمینه",
+ "paddingLabel": "فاصله داخلی",
+ "filenameLabel": "نام فایل",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "نقطههای پنجره",
+ "themeLabel": "پوسته پنجره",
+ "themeDark": "تیره",
+ "themeLight": "روشن",
+ "scaleLabel": "مقیاس خروجی",
+ "copyImage": "کپی تصویر",
+ "copied": "کپی شد",
+ "exportPng": "خروجی PNG",
+ "inputLabel": "کد",
+ "detected": "تشخیصدادهشده: {language}",
+ "clear": "پاک کردن",
+ "placeholder": "کد خود را اینجا بچسبانید…",
+ "previewLabel": "پیشنمایش",
+ "emptyPreview": "پیشنمایش کد شما اینجا ظاهر میشود",
+ "exportFailed": "خروجیگرفتن از تصویر ناموفق بود",
+ "copyUnsupported": "کپی تصاویر در این مرورگر پشتیبانی نمیشود",
+ "copyFailed": "کپی تصویر ناموفق بود",
+ "backgrounds": {
+ "ocean": "اقیانوس",
+ "sunset": "غروب",
+ "candy": "آبنبات",
+ "forest": "جنگل",
+ "midnight": "نیمهشب",
+ "gold": "طلایی",
+ "aurora": "شفق",
+ "slate": "سنگی",
+ "transparent": "شفاف"
+ }
+ },
+ "CssGenerators": {
+ "title": "مولدهای CSS",
+ "subtitle": "box-shadow، border-radius و clamp() سیّال — کد CSS را کپی کنید.",
+ "preview": "پیشنمایش",
+ "output": "CSS",
+ "copy": "کپی",
+ "copied": "کپی شد",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "clamp() سیّال"
+ },
+ "boxShadow": {
+ "presets": "پیشتنظیمها",
+ "layer": "لایه",
+ "addLayer": "افزودن لایه",
+ "removeLayer": "حذف لایه",
+ "x": "X",
+ "y": "Y",
+ "blur": "محو",
+ "spread": "گسترش",
+ "color": "رنگ",
+ "alpha": "آلفا",
+ "inset": "داخلی",
+ "presetNames": {
+ "subtleCard": "کارت ملایم",
+ "elevated": "برجسته",
+ "hardOffset": "افست تیز",
+ "neonGlow": "درخشش نئون",
+ "inner": "درونی",
+ "layeredSmooth": "چندلایه نرم"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "حالت",
+ "modes": {
+ "simple": "برای هر گوشه",
+ "blob": "شکل ارگانیک"
+ },
+ "unit": "واحد",
+ "linkCorners": "پیوند گوشهها",
+ "randomize": "تصادفی",
+ "horizontal": "شعاعهای افقی",
+ "vertical": "شعاعهای عمودی",
+ "corners": {
+ "tl": "بالا چپ",
+ "tr": "بالا راست",
+ "br": "پایین راست",
+ "bl": "پایین چپ"
+ }
+ },
+ "clamp": {
+ "minFont": "کمینه فونت (px)",
+ "maxFont": "بیشینه فونت (px)",
+ "minViewport": "کمینه viewport (px)",
+ "maxViewport": "بیشینه viewport (px)",
+ "rootFont": "فونت ریشه (px)",
+ "viewportWidth": "viewport شبیهسازیشده",
+ "computedSize": "اندازه محاسبهشده",
+ "slope": "شیب",
+ "intercept": "عرض از مبدأ",
+ "previewText": "متن نمونه برای پیشنمایش",
+ "invalid": "کرانهای معتبر و متفاوت viewport وارد کنید."
+ }
+ },
+ "ExifViewer": {
+ "title": "نمایشگر و حذفکننده EXIF",
+ "subtitle": "متادیتای عکس را بررسی و در مرورگر خود حذف کنید",
+ "dropTitle": "تصویری را اینجا رها کنید",
+ "dropHint": "یا برای مرور کلیک کنید — تصویر شما هرگز دستگاهتان را ترک نمیکند",
+ "browse": "انتخاب تصویر",
+ "formats": "JPEG، PNG، WebP، HEIC، TIFF",
+ "parsing": "در حال خواندن متادیتا…",
+ "parseFailed": "خواندن این تصویر ناموفق بود",
+ "clear": "حذف تصویر",
+ "gpsWarning": "حاوی موقعیت GPS است",
+ "openMap": "نمایش روی OpenStreetMap",
+ "noMetadata": "هیچ متادیتای تعبیهشدهای در این تصویر یافت نشد.",
+ "facts": {
+ "name": "نام",
+ "type": "نوع",
+ "dimensions": "ابعاد",
+ "size": "اندازه"
+ },
+ "groups": {
+ "camera": "دوربین",
+ "exposure": "نوردهی",
+ "timestamps": "برچسبهای زمانی",
+ "gps": "موقعیت GPS",
+ "software": "نرمافزار و سایر"
+ },
+ "gpsCard": {
+ "latitude": "عرض جغرافیایی",
+ "longitude": "طول جغرافیایی",
+ "altitude": "ارتفاع"
+ },
+ "raw": {
+ "title": "همه متادیتا",
+ "copyJson": "کپی JSON",
+ "copied": "کپی شد"
+ },
+ "remove": {
+ "title": "حذف متادیتا",
+ "jpegDescription": "EXIF، XMP، IPTC و کامنتها را بدون افت کیفیت و بدون فشردهسازی مجدد تصویر حذف کنید.",
+ "reencodeDescription": "تصویر را دوباره کدگذاری کنید تا متادیتای تعبیهشده حذف شود. این کار پیکسلها را دوباره فشرده میکند.",
+ "keepIcc": "حفظ پروفایل رنگ (ICC)",
+ "stripButton": "حذف متادیتا",
+ "reencodeButton": "کدگذاری مجدد و حذف",
+ "unsupported": "حذف متادیتا برای این نوع فایل پشتیبانی نمیشود.",
+ "failed": "حذف متادیتا ناموفق بود",
+ "before": "قبل",
+ "after": "بعد",
+ "download": "دانلود تصویر پاکشده"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "مولد Favicon",
+ "subtitle": "از یک تصویر، favicon و آیکونهای اپ بسازید",
+ "dropTitle": "تصویری را اینجا رها کنید",
+ "dropHint": "PNG، JPEG یا SVG — برای مرور کلیک کنید",
+ "browse": "انتخاب تصویر",
+ "clear": "حذف تصویر",
+ "backgroundLabel": "پسزمینه",
+ "transparent": "شفاف",
+ "solidColor": "یکرنگ",
+ "cornerLabel": "شکل گوشه",
+ "cornerNone": "مربع",
+ "cornerRounded": "گِرد",
+ "cornerCircle": "دایره",
+ "maskPaddingLabel": "فاصله داخلی maskable",
+ "maskable": "maskable",
+ "previewLabel": "پیشنمایش",
+ "copy": "کپی",
+ "headSnippet": "قطعهکد HTML head",
+ "downloadIco": "دانلود favicon.ico",
+ "downloadZip": "دانلود همه (.zip)"
+ },
+ "JsonDiff": {
+ "title": "تفاوت JSON",
+ "subtitle": "مقایسه معنایی و ساختارآگاه دو سند JSON — ترتیب کلیدها و قالببندی نادیده گرفته میشود",
+ "options": {
+ "ignoreArrayOrder": "نادیدهگرفتن ترتیب آرایه"
+ },
+ "loadBothSamples": "بارگذاری نمونهها",
+ "sample": "نمونه",
+ "format": "قالب",
+ "clear": "پاک کردن",
+ "formatError": "قالببندی ممکن نیست: ورودی JSON معتبر نیست",
+ "placeholder": "JSON را اینجا بچسبانید یا تایپ کنید…",
+ "panels": {
+ "left": "اصلی (چپ)",
+ "right": "تغییریافته (راست)",
+ "results": "تفاوتها"
+ },
+ "summary": {
+ "added": "{count} افزوده",
+ "removed": "{count} حذفشده",
+ "changed": "{count} تغییریافته"
+ },
+ "types": {
+ "added": "افزوده",
+ "removed": "حذفشده",
+ "changed": "تغییریافته"
+ },
+ "copyChanges": "کپی بهصورت JSON",
+ "copied": "کپی شد",
+ "empty": "JSON را در هر دو پنل بچسبانید تا تفاوتهایشان را ببینید.",
+ "identical": "دو سند از نظر معنایی یکسان هستند."
+ },
+ "JsonpathPlayground": {
+ "title": "زمین بازی JSONPath",
+ "subtitle": "اسناد JSON را با JSONPath یا JMESPath پرسوجو کنید و نتایج زنده را ببینید",
+ "engineLabel": "موتور",
+ "queryLabel": "پرسوجو",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "برگه تقلب",
+ "cheats": {
+ "jpAllTitles": "همه عنوانهای کتاب",
+ "jpAllAuthors": "هر نویسنده، در هر جای سند",
+ "jpFirstBook": "اولین کتاب",
+ "jpLastBook": "آخرین کتاب",
+ "jpSlice": "دو کتاب اول (برش)",
+ "jpFilterPrice": "کتابهای ارزانتر از ۴۰",
+ "jpFilterInStock": "عنوان کتابهای موجود",
+ "jpRecursive": "همه مقادیر، بهصورت بازگشتی",
+ "jmAllTitles": "همه عنوانهای کتاب",
+ "jmFirstAuthor": "نویسنده اولین کتاب",
+ "jmLastBook": "آخرین کتاب",
+ "jmSlice": "دو کتاب اول (برش)",
+ "jmFilterPrice": "عنوان کتابهای ارزانتر از ۴۰",
+ "jmCount": "شمارش کتابهای موجود",
+ "jmProjection": "بازآرایی هر کتاب به یک شیء جدید",
+ "jmSort": "عنوانها مرتبشده بر اساس قیمت"
+ },
+ "panels": {
+ "input": "ورودی JSON",
+ "results": "نتایج"
+ },
+ "sample": "نمونه",
+ "format": "قالب",
+ "clear": "پاک کردن",
+ "formatError": "قالببندی ممکن نیست: ورودی JSON معتبر نیست",
+ "placeholder": "JSON را اینجا بچسبانید یا تایپ کنید…",
+ "outputPlaceholder": "تطبیقها اینجا ظاهر میشوند…",
+ "matchCount": "{count, plural, one {# تطبیق} other {# تطبیق}}",
+ "copy": "کپی",
+ "copied": "کپی شد"
+ },
+ "KeycodeInspector": {
+ "title": "بازرس کدِ کلید",
+ "subtitle": "هر کلیدی را فشار دهید تا رویداد صفحهکلید JavaScript آن را بررسی کنید",
+ "capture": {
+ "prompt": "هر کلیدی را فشار دهید…",
+ "ready": "در حال گوشدادن — هر کلیدی روی صفحهکلید خود فشار دهید",
+ "clickToFocus": "برای فوکوس اینجا کلیک کنید، سپس هر کلیدی را فشار دهید"
+ },
+ "hero": {
+ "spaceKey": "(فاصله)",
+ "keyLabel": "event.key",
+ "repeat": "تکرار"
+ },
+ "copyJson": "کپی JSON",
+ "copiedJson": "JSON رویداد در کلیپبورد کپی شد",
+ "deprecated": "منسوخ",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "استاندارد",
+ "left": "چپ",
+ "right": "راست",
+ "numpad": "صفحهکلید عددی",
+ "unknown": "نامشخص"
+ },
+ "history": {
+ "title": "تاریخچه",
+ "clear": "پاککردن تاریخچه",
+ "empty": "فشردن کلیدها اینجا ظاهر میشود."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "مولد جدول Markdown",
+ "subtitle": "یک شبکه را ویرایش کنید، از صفحهگسترده بچسبانید و جدول Markdown یا HTML با تراز زیبا کپی کنید",
+ "addRow": "افزودن ردیف",
+ "addColumn": "افزودن ستون",
+ "importLabel": "درونریزی",
+ "importPlaceholder": "TSV (از Excel / Google Sheets) یا CSV را اینجا بچسبانید…",
+ "importButton": "درونریزی به شبکه",
+ "pasteClipboard": "چسباندن از کلیپبورد",
+ "importEmpty": "چیزی برای درونریزی نیست — ابتدا چند ردیف بچسبانید",
+ "clipboardError": "خواندن از کلیپبورد ممکن نشد",
+ "clear": "پاک کردن",
+ "panels": {
+ "editor": "ویرایشگر جدول",
+ "output": "خروجی"
+ },
+ "align": {
+ "left": "تراز چپ",
+ "center": "تراز وسط",
+ "right": "تراز راست"
+ },
+ "headerPlaceholder": "سرستون",
+ "cellPlaceholder": "سلول",
+ "removeRow": "حذف ردیف",
+ "removeColumn": "حذف ستون",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "کپی",
+ "copied": "کپی شد"
+ },
+ "TimezoneConverter": {
+ "title": "مبدل منطقه زمانی",
+ "subtitle": "یک لحظه را در سراسر جهان برای برنامهریزی جلسات تبدیل کنید.",
+ "dateTimeLabel": "تاریخ و زمان",
+ "now": "اکنون",
+ "sourceZoneLabel": "منطقه زمانی مبدأ",
+ "selectZone": "انتخاب منطقه",
+ "searchZone": "جستوجوی مناطق زمانی...",
+ "noZone": "منطقه زمانیای یافت نشد.",
+ "scrubLabel": "جابهجایی زمان",
+ "reset": "بازنشانی",
+ "targetsLabel": "مناطق مقصد",
+ "addZone": "افزودن منطقه",
+ "remove": "حذف",
+ "localBadge": "محلی",
+ "invalidDate": "تاریخ و زمان معتبری وارد کنید.",
+ "bands": {
+ "work": "ساعات کاری (۰۹:۰۰–۱۸:۰۰)",
+ "edge": "ساعات حاشیهای (صبح زود / عصر)",
+ "night": "شب"
+ }
+ },
+ "TokenCounter": {
+ "title": "شمارنده توکن LLM",
+ "subtitle": "توکنها را بشمارید، تکهها را تجسم کنید و هزینه API را تخمین بزنید.",
+ "inputLabel": "متن",
+ "sample": "نمونه",
+ "clear": "پاک کردن",
+ "placeholder": "متن، کد یا JSON را برای شمارش توکن بچسبانید...",
+ "encoding": "کدگذاری",
+ "loading": "در حال بارگذاری توکنایزر...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / o-series)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "توکنها",
+ "characters": "کاراکترها",
+ "words": "کلمات",
+ "ratio": "توکن/کلمه",
+ "ratioSub": "تقریبی"
+ },
+ "claudeRow": "Claude (تقریبی)",
+ "estimate": "تخمین",
+ "viz": {
+ "title": "تجسم توکن",
+ "toggle": "نمایش",
+ "hint": "برای رنگآمیزی هر توکن بهعنوان یک تکه، تغییر وضعیت دهید.",
+ "empty": "برای تجسم توکنها متن وارد کنید.",
+ "truncated": "نمایش {limit} توکن اول از {total} توکن."
+ },
+ "cost": {
+ "title": "ماشینحساب هزینه",
+ "disclaimer": "مقادیر پیشفرض قابلویرایش — قیمتگذاری فعلی را بررسی کنید",
+ "model": "مدل",
+ "expectedOutput": "توکنهای خروجی موردانتظار",
+ "customInput": "ورودی $/MTok",
+ "customOutput": "خروجی $/MTok",
+ "inputCost": "هزینه ورودی",
+ "outputCost": "هزینه خروجی",
+ "total": "کل",
+ "per1000": "بهازای هر ۱۰۰۰ فراخوانی"
+ }
+ },
+ "WebhookTester": {
+ "title": "آزمایشگر Webhook",
+ "subtitle": "یک URL موقت بسازید و درخواستهای ورودی webhook را زنده بررسی کنید.",
+ "intro": {
+ "title": "برای شروع ثبت درخواستها یک URL webhook بسازید",
+ "hint": "یک URL غیرقابلحدس دریافت میکنید که هر درخواست ورودی را ثبت میکند. پس از ۲۴ ساعت منقضی میشود."
+ },
+ "createBtn": "ساخت URL webhook",
+ "creating": "در حال ساخت…",
+ "urlLabel": "URL webhook شما",
+ "expiresIn": "انقضا در {countdown}",
+ "copyUrlBtn": "کپی URL",
+ "copied": "کپی شد",
+ "clearBtn": "پاک کردن",
+ "newBinBtn": "URL جدید",
+ "requestsTitle": "درخواستها",
+ "selectPrompt": "یک درخواست را برای بررسی جزئیاتش انتخاب کنید.",
+ "expiredNotice": "این URL webhook منقضی شده است. برای ادامه آزمایش یک URL جدید بسازید.",
+ "base64Badge": "base64",
+ "truncatedBadge": "کوتاهشده",
+ "empty": {
+ "title": "در انتظار درخواستها…",
+ "hint": "درخواستی به URL خود بفرستید تا فوراً اینجا ظاهر شود. این را امتحان کنید:"
+ },
+ "detail": {
+ "receivedAt": "دریافتشده",
+ "source": "IP مبدأ",
+ "contentType": "Content-Type",
+ "query": "پارامترهای پرسوجو",
+ "headers": "هدرها",
+ "body": "بدنه",
+ "copyBody": "کپی بدنه",
+ "emptyBody": "بدون بدنه"
+ },
+ "errors": {
+ "createFailed": "ساخت URL webhook ناموفق بود. لطفاً دوباره تلاش کنید.",
+ "clearFailed": "پاککردن درخواستها ناموفق بود. لطفاً دوباره تلاش کنید."
+ }
+ },
+ "WebsocketTester": {
+ "title": "آزمایشگر WebSocket",
+ "subtitle": "به یک نقطه پایانی WebSocket متصل شوید و پیامها را زنده بفرستید، دریافت و بررسی کنید",
+ "urlLabel": "URL نقطه پایانی",
+ "protocolsLabel": "زیرپروتکلها",
+ "protocolsPlaceholder": "اختیاری، جداشده با کاما",
+ "connect": "اتصال",
+ "disconnect": "قطع اتصال",
+ "status": {
+ "disconnected": "قطعشده",
+ "connecting": "در حال اتصال",
+ "connected": "متصل",
+ "error": "خطا"
+ },
+ "messages": {
+ "connecting": "در حال اتصال به {url}…",
+ "connected": "اتصال برقرار شد",
+ "connectedWithProtocol": "اتصال برقرار شد (پروتکل: {protocol})",
+ "invalidUrl": "URL نامعتبر — باید با ws:// یا wss:// شروع شود",
+ "connectFailed": "بازکردن اتصال ناموفق بود",
+ "socketError": "خطایی در اتصال رخ داد",
+ "notConnected": "متصل نیست — ابتدا متصل شوید",
+ "binaryMessage": "یک پیام باینری دریافت شد"
+ },
+ "log": {
+ "title": "گزارش پیامها",
+ "empty": "هنوز پیامی نیست. متصل شوید و ارسال را آغاز کنید.",
+ "copyAll": "کپی همه",
+ "download": "دانلود بهصورت .txt",
+ "clear": "پاککردن گزارش",
+ "prettyPrint": "قالببندی JSON",
+ "showRaw": "نمایش متن خام"
+ },
+ "composer": {
+ "placeholder": "پیامی برای ارسال بنویسید…",
+ "send": "ارسال",
+ "formatJson": "قالببندی بهصورت JSON",
+ "invalidJson": "پیام JSON معتبر نیست",
+ "hint": "Cmd/Ctrl+Enter برای ارسال"
+ },
+ "ping": {
+ "label": "ping خودکار",
+ "seconds": "ثانیه",
+ "intervalTitle": "فاصله ping به ثانیه",
+ "payloadPlaceholder": "محموله ping"
+ }
+ },
+ "WhoisLookup": {
+ "title": "جستوجوی Whois",
+ "subtitle": "جزئیات ثبت هر دامنه یا آدرس IP را از طریق RDAP جستوجو کنید",
+ "placeholder": "example.com یا 8.8.8.8",
+ "lookupBtn": "جستوجو",
+ "looking": "در حال جستوجو…",
+ "refreshBtn": "تازهسازی",
+ "tryLabel": "امتحان کنید:",
+ "copy": "کپی",
+ "copied": "کپی شد",
+ "notAvailable": "در دسترس نیست",
+ "summary": {
+ "registrar": "ثبتکننده",
+ "network": "شبکه"
+ },
+ "dates": {
+ "created": "ایجادشده",
+ "updated": "بهروزشده",
+ "expires": "انقضا"
+ },
+ "sections": {
+ "status": "وضعیت",
+ "nameservers": "سرورهای نام",
+ "contacts": "تماسها",
+ "rawRdap": "RDAP خام"
+ },
+ "contacts": {
+ "roles": "نقشها",
+ "name": "نام",
+ "organization": "سازمان"
+ },
+ "errors": {
+ "invalidTarget": "یک نام دامنه یا آدرس IP معتبر وارد کنید",
+ "lookupFailed": "جستوجو ناموفق بود (وضعیت {status})"
+ }
}
}
diff --git a/apps/web/messages/fr.json b/apps/web/messages/fr.json
index df0db479..f7c9f1c8 100644
--- a/apps/web/messages/fr.json
+++ b/apps/web/messages/fr.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Calculateur Chmod",
+ "codeScreenshot": "Capture de code",
+ "cssGenerators": "Générateurs CSS",
+ "exifViewer": "Visionneuse EXIF",
+ "faviconGenerator": "Générateur de favicon",
+ "jsonDiff": "Diff JSON",
+ "jsonpathPlayground": "Playground JSONPath",
+ "keycodeInspector": "Inspecteur de keycode",
+ "markdownTableGenerator": "Tableau Markdown",
+ "timezoneConverter": "Convertisseur de fuseaux",
+ "tokenCounter": "Compteur de tokens",
+ "webhookTester": "Testeur de webhook",
+ "websocketTester": "Testeur WebSocket",
+ "whoisLookup": "Recherche Whois"
},
"Help": {
"title": "Aide et documentation",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Générateur et vérificateur Bcrypt",
+ "description": "Hachez des mots de passe avec bcrypt et vérifiez des hachages existants."
+ },
+ "chmodCalculator": {
+ "title": "Calculateur Chmod",
+ "description": "Convertissez les permissions Unix entre octal, symbolique et cases à cocher."
+ },
+ "codeScreenshot": {
+ "title": "Capture de code",
+ "description": "Transformez votre code source en images élégantes et partageables."
+ },
+ "cssGenerators": {
+ "title": "Générateurs CSS",
+ "description": "Générateurs de box-shadow, border-radius et clamp() fluide."
+ },
+ "exifViewer": {
+ "title": "Visionneuse et nettoyeur EXIF",
+ "description": "Inspectez et supprimez les métadonnées EXIF des photos en local."
+ },
+ "faviconGenerator": {
+ "title": "Générateur de favicon",
+ "description": "Générez des favicons, icônes d'application et extraits de manifeste."
+ },
+ "jsonDiff": {
+ "title": "Diff JSON",
+ "description": "Comparaison sémantique tenant compte de la structure de deux documents JSON."
+ },
+ "jsonpathPlayground": {
+ "title": "Playground JSONPath",
+ "description": "Interrogez du JSON avec JSONPath ou JMESPath et voyez les résultats en direct."
+ },
+ "keycodeInspector": {
+ "title": "Inspecteur de keycode",
+ "description": "Appuyez sur une touche pour voir ses codes d'événement JavaScript et ses modificateurs."
+ },
+ "markdownTableGenerator": {
+ "title": "Générateur de tableaux Markdown",
+ "description": "Créez des tableaux Markdown et HTML dans un éditeur en grille de type tableur."
+ },
+ "timezoneConverter": {
+ "title": "Convertisseur de fuseaux horaires",
+ "description": "Convertissez une heure sur de nombreux fuseaux avec repères de décalage de jour et d'heures ouvrées."
+ },
+ "tokenCounter": {
+ "title": "Compteur de tokens LLM",
+ "description": "Comptez les tokens LLM, visualisez les segments et estimez le coût de l'API."
+ },
+ "webhookTester": {
+ "title": "Testeur de webhook",
+ "description": "Capturez et inspectez les requêtes webhook entrantes en temps réel."
+ },
+ "websocketTester": {
+ "title": "Testeur WebSocket",
+ "description": "Connectez-vous à des points de terminaison WebSocket et échangez des messages en temps réel."
+ },
+ "whoisLookup": {
+ "title": "Recherche Whois",
+ "description": "Consultez les détails d'enregistrement d'un domaine ou d'une IP via RDAP."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Générateur et vérificateur Bcrypt",
+ "subtitle": "Hachez des mots de passe avec bcrypt et vérifiez des hachages — tout reste dans votre navigateur",
+ "tabs": {
+ "hash": "Hacher",
+ "verify": "Vérifier"
+ },
+ "showPassword": "Afficher le mot de passe",
+ "hidePassword": "Masquer le mot de passe",
+ "copy": "Copier",
+ "copied": "Copié",
+ "hash": {
+ "passwordLabel": "Mot de passe",
+ "passwordPlaceholder": "Saisissez un mot de passe à hacher…",
+ "costLabel": "Facteur de coût",
+ "costHint": "Un coût plus élevé double le travail par étape. 10-12 est un choix courant en production.",
+ "slowWarning": "Les facteurs de coût de 13 et plus peuvent prendre plusieurs secondes à calculer.",
+ "generate": "Générer le hachage",
+ "generating": "Hachage…",
+ "resultLabel": "Hachage bcrypt",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "Anatomie du hachage",
+ "algorithm": "algorithme",
+ "cost": "coût",
+ "salt": "sel (22 caractères)",
+ "hash": "hachage (31 caractères)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Mot de passe",
+ "passwordPlaceholder": "Saisissez le mot de passe à vérifier…",
+ "hashLabel": "Hachage bcrypt",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Saisissez un mot de passe et un hachage bcrypt pour les comparer.",
+ "invalidHash": "Ceci ne ressemble pas à un hachage bcrypt valide.",
+ "checking": "Comparaison…",
+ "match": "Correspondance — le mot de passe produit ce hachage.",
+ "noMatch": "Aucune correspondance — le mot de passe ne produit pas ce hachage."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Calculateur Chmod",
+ "subtitle": "Convertissez les permissions de fichiers Unix entre cases à cocher, octal et notation symbolique",
+ "grid": {
+ "title": "Permissions",
+ "read": "Lecture",
+ "write": "Écriture",
+ "execute": "Exécution",
+ "owner": "Propriétaire",
+ "group": "Groupe",
+ "others": "Autres"
+ },
+ "special": {
+ "title": "Bits spéciaux",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "Octal",
+ "placeholder": "755 ou 4755",
+ "invalid": "Saisissez 3 ou 4 chiffres octaux (0-7)."
+ },
+ "symbolic": {
+ "label": "Symbolique",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Saisissez 9 caractères comme rwxr-xr-x (s/S, t/T autorisés)."
+ },
+ "output": {
+ "title": "Commande",
+ "copy": "Copier",
+ "copied": "Copié"
+ },
+ "explain": {
+ "line": "{cls} peut {perms}.",
+ "none": "{cls} n'a aucune permission.",
+ "setuid": "Setuid : le fichier s'exécute avec les privilèges de son propriétaire.",
+ "setgid": "Setgid : le fichier s'exécute avec les privilèges de son groupe (les nouveaux fichiers d'un répertoire héritent de son groupe).",
+ "sticky": "Sticky : seul le propriétaire d'un fichier peut le supprimer ou le renommer dans ce répertoire."
+ },
+ "presets": {
+ "title": "Préréglages courants",
+ "octal": "Octal",
+ "permissions": "Permissions",
+ "description": "Usage typique",
+ "p644": "Fichiers ordinaires : le propriétaire écrit, tout le monde lit.",
+ "p755": "Exécutables et répertoires : tout le monde peut lire et entrer.",
+ "p600": "Fichiers privés : seul le propriétaire lit et écrit.",
+ "p700": "Exécutables et répertoires privés : propriétaire uniquement.",
+ "p664": "Fichiers modifiables par le groupe.",
+ "p775": "Répertoires et exécutables modifiables par le groupe.",
+ "p400": "Lecture seule pour le propriétaire (ex. clés privées SSH)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Capture de code",
+ "subtitle": "Transformez votre code en images élégantes et partageables",
+ "languageLabel": "Langage",
+ "autoDetect": "Détection auto",
+ "backgroundLabel": "Arrière-plan",
+ "paddingLabel": "Marge intérieure",
+ "filenameLabel": "Nom de fichier",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "Points de fenêtre",
+ "themeLabel": "Thème de fenêtre",
+ "themeDark": "Sombre",
+ "themeLight": "Clair",
+ "scaleLabel": "Échelle d'export",
+ "copyImage": "Copier l'image",
+ "copied": "Copié",
+ "exportPng": "Exporter en PNG",
+ "inputLabel": "Code",
+ "detected": "Détecté : {language}",
+ "clear": "Effacer",
+ "placeholder": "Collez votre code ici…",
+ "previewLabel": "Aperçu",
+ "emptyPreview": "L'aperçu de votre code apparaîtra ici",
+ "exportFailed": "Échec de l'export de l'image",
+ "copyUnsupported": "La copie d'images n'est pas prise en charge dans ce navigateur",
+ "copyFailed": "Échec de la copie de l'image",
+ "backgrounds": {
+ "ocean": "Océan",
+ "sunset": "Coucher de soleil",
+ "candy": "Bonbon",
+ "forest": "Forêt",
+ "midnight": "Minuit",
+ "gold": "Or",
+ "aurora": "Aurore",
+ "slate": "Ardoise",
+ "transparent": "Transparent"
+ }
+ },
+ "CssGenerators": {
+ "title": "Générateurs CSS",
+ "subtitle": "Box-shadow, border-radius et clamp() fluide — copiez le CSS.",
+ "preview": "Aperçu",
+ "output": "CSS",
+ "copy": "Copier",
+ "copied": "Copié",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "clamp() fluide"
+ },
+ "boxShadow": {
+ "presets": "Préréglages",
+ "layer": "Calque",
+ "addLayer": "Ajouter un calque",
+ "removeLayer": "Supprimer le calque",
+ "x": "X",
+ "y": "Y",
+ "blur": "Flou",
+ "spread": "Étendue",
+ "color": "Couleur",
+ "alpha": "Alpha",
+ "inset": "Intérieur",
+ "presetNames": {
+ "subtleCard": "Carte subtile",
+ "elevated": "Surélevé",
+ "hardOffset": "Décalage net",
+ "neonGlow": "Lueur néon",
+ "inner": "Intérieur",
+ "layeredSmooth": "Superposé doux"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Mode",
+ "modes": {
+ "simple": "Par coin",
+ "blob": "Blob organique"
+ },
+ "unit": "Unité",
+ "linkCorners": "Lier les coins",
+ "randomize": "Aléatoire",
+ "horizontal": "Rayons horizontaux",
+ "vertical": "Rayons verticaux",
+ "corners": {
+ "tl": "Haut gauche",
+ "tr": "Haut droite",
+ "br": "Bas droite",
+ "bl": "Bas gauche"
+ }
+ },
+ "clamp": {
+ "minFont": "Police min (px)",
+ "maxFont": "Police max (px)",
+ "minViewport": "Viewport min (px)",
+ "maxViewport": "Viewport max (px)",
+ "rootFont": "Police racine (px)",
+ "viewportWidth": "Viewport simulé",
+ "computedSize": "Taille calculée",
+ "slope": "Pente",
+ "intercept": "Ordonnée à l'origine",
+ "previewText": "Portez ce vieux whisky au juge blond",
+ "invalid": "Saisissez des bornes de viewport valides et différentes."
+ }
+ },
+ "ExifViewer": {
+ "title": "Visionneuse et nettoyeur EXIF",
+ "subtitle": "Inspectez les métadonnées des photos et supprimez-les dans votre navigateur",
+ "dropTitle": "Déposez une image ici",
+ "dropHint": "ou cliquez pour parcourir — votre image ne quitte jamais votre appareil",
+ "browse": "Choisir une image",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Lecture des métadonnées…",
+ "parseFailed": "Échec de la lecture de cette image",
+ "clear": "Supprimer l'image",
+ "gpsWarning": "Contient une position GPS",
+ "openMap": "Voir sur OpenStreetMap",
+ "noMetadata": "Aucune métadonnée intégrée n'a été trouvée dans cette image.",
+ "facts": {
+ "name": "Nom",
+ "type": "Type",
+ "dimensions": "Dimensions",
+ "size": "Taille"
+ },
+ "groups": {
+ "camera": "Appareil photo",
+ "exposure": "Exposition",
+ "timestamps": "Horodatages",
+ "gps": "Position GPS",
+ "software": "Logiciel et autres"
+ },
+ "gpsCard": {
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "altitude": "Altitude"
+ },
+ "raw": {
+ "title": "Toutes les métadonnées",
+ "copyJson": "Copier le JSON",
+ "copied": "Copié"
+ },
+ "remove": {
+ "title": "Supprimer les métadonnées",
+ "jpegDescription": "Supprimez sans perte les données EXIF, XMP, IPTC et les commentaires sans recompresser l'image.",
+ "reencodeDescription": "Réencodez l'image pour supprimer les métadonnées intégrées. Cela recompresse les pixels.",
+ "keepIcc": "Conserver le profil colorimétrique (ICC)",
+ "stripButton": "Supprimer les métadonnées",
+ "reencodeButton": "Réencoder et nettoyer",
+ "unsupported": "La suppression des métadonnées n'est pas prise en charge pour ce type de fichier.",
+ "failed": "Échec de la suppression des métadonnées",
+ "before": "Avant",
+ "after": "Après",
+ "download": "Télécharger l'image nettoyée"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Générateur de favicon",
+ "subtitle": "Créez des favicons et icônes d'application à partir d'une seule image",
+ "dropTitle": "Déposez une image ici",
+ "dropHint": "PNG, JPEG ou SVG — cliquez pour parcourir",
+ "browse": "Choisir une image",
+ "clear": "Supprimer l'image",
+ "backgroundLabel": "Arrière-plan",
+ "transparent": "Transparent",
+ "solidColor": "Uni",
+ "cornerLabel": "Forme des coins",
+ "cornerNone": "Carré",
+ "cornerRounded": "Arrondi",
+ "cornerCircle": "Cercle",
+ "maskPaddingLabel": "Marge maskable",
+ "maskable": "maskable",
+ "previewLabel": "Aperçu",
+ "copy": "Copier",
+ "headSnippet": "Extrait pour le head HTML",
+ "downloadIco": "Télécharger favicon.ico",
+ "downloadZip": "Tout télécharger (.zip)"
+ },
+ "JsonDiff": {
+ "title": "Diff JSON",
+ "subtitle": "Comparaison sémantique tenant compte de la structure de deux documents JSON — ordre des clés et formatage ignorés",
+ "options": {
+ "ignoreArrayOrder": "Ignorer l'ordre des tableaux"
+ },
+ "loadBothSamples": "Charger des exemples",
+ "sample": "Exemple",
+ "format": "Formater",
+ "clear": "Effacer",
+ "formatError": "Impossible de formater : l'entrée n'est pas un JSON valide",
+ "placeholder": "Collez ou saisissez du JSON ici…",
+ "panels": {
+ "left": "Original (gauche)",
+ "right": "Modifié (droite)",
+ "results": "Différences"
+ },
+ "summary": {
+ "added": "{count} ajouté(s)",
+ "removed": "{count} supprimé(s)",
+ "changed": "{count} modifié(s)"
+ },
+ "types": {
+ "added": "Ajouté",
+ "removed": "Supprimé",
+ "changed": "Modifié"
+ },
+ "copyChanges": "Copier en JSON",
+ "copied": "Copié",
+ "empty": "Collez du JSON dans les deux panneaux pour voir leurs différences.",
+ "identical": "Les deux documents sont sémantiquement identiques."
+ },
+ "JsonpathPlayground": {
+ "title": "Playground JSONPath",
+ "subtitle": "Interrogez des documents JSON avec JSONPath ou JMESPath et voyez les résultats en direct",
+ "engineLabel": "Moteur",
+ "queryLabel": "Requête",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Aide-mémoire",
+ "cheats": {
+ "jpAllTitles": "Tous les titres de livres",
+ "jpAllAuthors": "Chaque auteur, n'importe où dans le document",
+ "jpFirstBook": "Premier livre",
+ "jpLastBook": "Dernier livre",
+ "jpSlice": "Les deux premiers livres (slice)",
+ "jpFilterPrice": "Livres à moins de 40",
+ "jpFilterInStock": "Titres des livres en stock",
+ "jpRecursive": "Toutes les valeurs, récursivement",
+ "jmAllTitles": "Tous les titres de livres",
+ "jmFirstAuthor": "Auteur du premier livre",
+ "jmLastBook": "Dernier livre",
+ "jmSlice": "Les deux premiers livres (slice)",
+ "jmFilterPrice": "Titres des livres à moins de 40",
+ "jmCount": "Compter les livres en stock",
+ "jmProjection": "Reformer chaque livre en un nouvel objet",
+ "jmSort": "Titres triés par prix"
+ },
+ "panels": {
+ "input": "Entrée JSON",
+ "results": "Résultats"
+ },
+ "sample": "Exemple",
+ "format": "Formater",
+ "clear": "Effacer",
+ "formatError": "Impossible de formater : l'entrée n'est pas un JSON valide",
+ "placeholder": "Collez ou saisissez du JSON ici…",
+ "outputPlaceholder": "Les correspondances apparaîtront ici…",
+ "matchCount": "{count, plural, one {# correspondance} other {# correspondances}}",
+ "copy": "Copier",
+ "copied": "Copié"
+ },
+ "KeycodeInspector": {
+ "title": "Inspecteur de keycode",
+ "subtitle": "Appuyez sur n'importe quelle touche pour inspecter son événement clavier JavaScript",
+ "capture": {
+ "prompt": "Appuyez sur une touche…",
+ "ready": "À l'écoute — appuyez sur une touche de votre clavier",
+ "clickToFocus": "Cliquez ici pour donner le focus, puis appuyez sur une touche"
+ },
+ "hero": {
+ "spaceKey": "(Espace)",
+ "keyLabel": "event.key",
+ "repeat": "répétition"
+ },
+ "copyJson": "Copier le JSON",
+ "copiedJson": "JSON de l'événement copié dans le presse-papiers",
+ "deprecated": "obsolète",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Standard",
+ "left": "Gauche",
+ "right": "Droite",
+ "numpad": "Pavé numérique",
+ "unknown": "Inconnu"
+ },
+ "history": {
+ "title": "Historique",
+ "clear": "Effacer l'historique",
+ "empty": "Les appuis sur les touches apparaîtront ici."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Générateur de tableaux Markdown",
+ "subtitle": "Modifiez une grille, collez depuis un tableur et copiez un tableau Markdown ou HTML joliment aligné",
+ "addRow": "Ajouter une ligne",
+ "addColumn": "Ajouter une colonne",
+ "importLabel": "Importer",
+ "importPlaceholder": "Collez du TSV (depuis Excel / Google Sheets) ou du CSV ici…",
+ "importButton": "Importer dans la grille",
+ "pasteClipboard": "Coller depuis le presse-papiers",
+ "importEmpty": "Rien à importer — collez d'abord quelques lignes",
+ "clipboardError": "Impossible de lire le presse-papiers",
+ "clear": "Effacer",
+ "panels": {
+ "editor": "Éditeur de tableau",
+ "output": "Résultat"
+ },
+ "align": {
+ "left": "Aligner à gauche",
+ "center": "Centrer",
+ "right": "Aligner à droite"
+ },
+ "headerPlaceholder": "En-tête",
+ "cellPlaceholder": "Cellule",
+ "removeRow": "Supprimer la ligne",
+ "removeColumn": "Supprimer la colonne",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Copier",
+ "copied": "Copié"
+ },
+ "TimezoneConverter": {
+ "title": "Convertisseur de fuseaux horaires",
+ "subtitle": "Convertissez un moment à travers le monde pour planifier vos réunions.",
+ "dateTimeLabel": "Date et heure",
+ "now": "Maintenant",
+ "sourceZoneLabel": "Fuseau source",
+ "selectZone": "Sélectionner un fuseau",
+ "searchZone": "Rechercher des fuseaux...",
+ "noZone": "Aucun fuseau trouvé.",
+ "scrubLabel": "Décaler l'heure",
+ "reset": "Réinitialiser",
+ "targetsLabel": "Fuseaux cibles",
+ "addZone": "Ajouter un fuseau",
+ "remove": "Supprimer",
+ "localBadge": "Local",
+ "invalidDate": "Saisissez une date et une heure valides.",
+ "bands": {
+ "work": "Heures ouvrées (09:00–18:00)",
+ "edge": "Heures limites (tôt / soir)",
+ "night": "Nuit"
+ }
+ },
+ "TokenCounter": {
+ "title": "Compteur de tokens LLM",
+ "subtitle": "Comptez les tokens, visualisez les segments et estimez le coût de l'API.",
+ "inputLabel": "Texte",
+ "sample": "Exemple",
+ "clear": "Effacer",
+ "placeholder": "Collez du texte, du code ou du JSON pour compter les tokens...",
+ "encoding": "Encodage",
+ "loading": "Chargement du tokeniseur...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / série o)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Tokens",
+ "characters": "Caractères",
+ "words": "Mots",
+ "ratio": "Tokens/mot",
+ "ratioSub": "approx."
+ },
+ "claudeRow": "Claude (approximatif)",
+ "estimate": "estimation",
+ "viz": {
+ "title": "Visualisation des tokens",
+ "toggle": "Afficher",
+ "hint": "Activez pour colorer chaque token comme un segment.",
+ "empty": "Saisissez du texte pour visualiser les tokens.",
+ "truncated": "Affichage des {limit} premiers tokens sur {total}."
+ },
+ "cost": {
+ "title": "Calculateur de coût",
+ "disclaimer": "valeurs par défaut modifiables — vérifiez les tarifs actuels",
+ "model": "Modèle",
+ "expectedOutput": "Tokens de sortie attendus",
+ "customInput": "Entrée $/MTok",
+ "customOutput": "Sortie $/MTok",
+ "inputCost": "Coût d'entrée",
+ "outputCost": "Coût de sortie",
+ "total": "Total",
+ "per1000": "Pour 1000 appels"
+ }
+ },
+ "WebhookTester": {
+ "title": "Testeur de webhook",
+ "subtitle": "Créez une URL temporaire et inspectez les requêtes webhook entrantes en direct.",
+ "intro": {
+ "title": "Créez une URL de webhook pour commencer à capturer les requêtes",
+ "hint": "Vous obtiendrez une URL non devinable qui enregistre chaque requête entrante. Elle expire au bout de 24 heures."
+ },
+ "createBtn": "Créer une URL de webhook",
+ "creating": "Création…",
+ "urlLabel": "Votre URL de webhook",
+ "expiresIn": "Expire dans {countdown}",
+ "copyUrlBtn": "Copier l'URL",
+ "copied": "Copié",
+ "clearBtn": "Effacer",
+ "newBinBtn": "Nouvelle URL",
+ "requestsTitle": "Requêtes",
+ "selectPrompt": "Sélectionnez une requête pour en inspecter les détails.",
+ "expiredNotice": "Cette URL de webhook a expiré. Créez-en une nouvelle pour continuer les tests.",
+ "base64Badge": "base64",
+ "truncatedBadge": "tronqué",
+ "empty": {
+ "title": "En attente de requêtes…",
+ "hint": "Envoyez une requête à votre URL et elle apparaîtra ici instantanément. Essayez ceci :"
+ },
+ "detail": {
+ "receivedAt": "Reçu",
+ "source": "IP source",
+ "contentType": "Content-Type",
+ "query": "Paramètres de requête",
+ "headers": "En-têtes",
+ "body": "Corps",
+ "copyBody": "Copier le corps",
+ "emptyBody": "Aucun corps"
+ },
+ "errors": {
+ "createFailed": "Échec de la création de l'URL de webhook. Veuillez réessayer.",
+ "clearFailed": "Échec de l'effacement des requêtes. Veuillez réessayer."
+ }
+ },
+ "WebsocketTester": {
+ "title": "Testeur WebSocket",
+ "subtitle": "Connectez-vous à un point de terminaison WebSocket et envoyez, recevez et inspectez des messages en direct",
+ "urlLabel": "URL du point de terminaison",
+ "protocolsLabel": "Sous-protocoles",
+ "protocolsPlaceholder": "facultatif, séparés par des virgules",
+ "connect": "Connecter",
+ "disconnect": "Déconnecter",
+ "status": {
+ "disconnected": "Déconnecté",
+ "connecting": "Connexion",
+ "connected": "Connecté",
+ "error": "Erreur"
+ },
+ "messages": {
+ "connecting": "Connexion à {url}…",
+ "connected": "Connexion établie",
+ "connectedWithProtocol": "Connexion établie (protocole : {protocol})",
+ "invalidUrl": "URL invalide — doit commencer par ws:// ou wss://",
+ "connectFailed": "Échec de l'ouverture de la connexion",
+ "socketError": "Une erreur de connexion s'est produite",
+ "notConnected": "Non connecté — connectez-vous d'abord",
+ "binaryMessage": "Message binaire reçu"
+ },
+ "log": {
+ "title": "Journal des messages",
+ "empty": "Aucun message pour l'instant. Connectez-vous et commencez à envoyer.",
+ "copyAll": "Tout copier",
+ "download": "Télécharger en .txt",
+ "clear": "Effacer le journal",
+ "prettyPrint": "Formater le JSON",
+ "showRaw": "Afficher le texte brut"
+ },
+ "composer": {
+ "placeholder": "Saisissez un message à envoyer…",
+ "send": "Envoyer",
+ "formatJson": "Formater en JSON",
+ "invalidJson": "Le message n'est pas un JSON valide",
+ "hint": "Cmd/Ctrl+Entrée pour envoyer"
+ },
+ "ping": {
+ "label": "Auto-ping",
+ "seconds": "s",
+ "intervalTitle": "Intervalle de ping en secondes",
+ "payloadPlaceholder": "charge utile du ping"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Recherche Whois",
+ "subtitle": "Consultez les détails d'enregistrement de n'importe quel domaine ou adresse IP via RDAP",
+ "placeholder": "example.com ou 8.8.8.8",
+ "lookupBtn": "Rechercher",
+ "looking": "Recherche…",
+ "refreshBtn": "Actualiser",
+ "tryLabel": "Essayez :",
+ "copy": "Copier",
+ "copied": "Copié",
+ "notAvailable": "Non disponible",
+ "summary": {
+ "registrar": "Bureau d'enregistrement",
+ "network": "Réseau"
+ },
+ "dates": {
+ "created": "Créé",
+ "updated": "Mis à jour",
+ "expires": "Expire"
+ },
+ "sections": {
+ "status": "Statut",
+ "nameservers": "Serveurs de noms",
+ "contacts": "Contacts",
+ "rawRdap": "RDAP brut"
+ },
+ "contacts": {
+ "roles": "Rôles",
+ "name": "Nom",
+ "organization": "Organisation"
+ },
+ "errors": {
+ "invalidTarget": "Saisissez un nom de domaine ou une adresse IP valide",
+ "lookupFailed": "Échec de la recherche (statut {status})"
+ }
}
}
diff --git a/apps/web/messages/id.json b/apps/web/messages/id.json
index d19794a0..685ce44f 100644
--- a/apps/web/messages/id.json
+++ b/apps/web/messages/id.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Kalkulator Chmod",
+ "codeScreenshot": "Tangkapan Kode",
+ "cssGenerators": "Generator CSS",
+ "exifViewer": "Penampil EXIF",
+ "faviconGenerator": "Generator Favicon",
+ "jsonDiff": "Diff JSON",
+ "jsonpathPlayground": "Playground JSONPath",
+ "keycodeInspector": "Inspektur Keycode",
+ "markdownTableGenerator": "Tabel Markdown",
+ "timezoneConverter": "Konverter Zona Waktu",
+ "tokenCounter": "Penghitung Token",
+ "webhookTester": "Penguji Webhook",
+ "websocketTester": "Penguji WebSocket",
+ "whoisLookup": "Pencarian Whois"
},
"Help": {
"title": "Bantuan & dokumentasi",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Generator & Pemeriksa Bcrypt",
+ "description": "Hash kata sandi dengan bcrypt dan verifikasi hash yang ada."
+ },
+ "chmodCalculator": {
+ "title": "Kalkulator Chmod",
+ "description": "Konversi izin Unix antara oktal, simbolik, dan kotak centang."
+ },
+ "codeScreenshot": {
+ "title": "Tangkapan Kode",
+ "description": "Ubah kode sumber menjadi gambar indah yang mudah dibagikan."
+ },
+ "cssGenerators": {
+ "title": "Generator CSS",
+ "description": "Generator box-shadow, border-radius, dan clamp() fluid."
+ },
+ "exifViewer": {
+ "title": "Penampil & Penghapus EXIF",
+ "description": "Periksa dan hapus metadata EXIF foto secara lokal."
+ },
+ "faviconGenerator": {
+ "title": "Generator Favicon",
+ "description": "Buat favicon, ikon aplikasi, dan cuplikan manifest."
+ },
+ "jsonDiff": {
+ "title": "Diff JSON",
+ "description": "Perbandingan dua dokumen JSON secara semantik dan sadar struktur."
+ },
+ "jsonpathPlayground": {
+ "title": "Playground JSONPath",
+ "description": "Kueri JSON dengan JSONPath atau JMESPath dan lihat hasilnya secara langsung."
+ },
+ "keycodeInspector": {
+ "title": "Inspektur Keycode",
+ "description": "Tekan tombol untuk melihat kode event JavaScript dan tombol pengubahnya."
+ },
+ "markdownTableGenerator": {
+ "title": "Generator Tabel Markdown",
+ "description": "Buat tabel Markdown dan HTML di editor grid mirip spreadsheet."
+ },
+ "timezoneConverter": {
+ "title": "Konverter Zona Waktu",
+ "description": "Konversi satu waktu ke banyak zona dengan petunjuk pergeseran hari dan jam kerja."
+ },
+ "tokenCounter": {
+ "title": "Penghitung Token LLM",
+ "description": "Hitung token LLM, visualisasikan potongan, dan perkirakan biaya API."
+ },
+ "webhookTester": {
+ "title": "Penguji Webhook",
+ "description": "Tangkap dan periksa permintaan webhook masuk secara real time."
+ },
+ "websocketTester": {
+ "title": "Penguji WebSocket",
+ "description": "Sambungkan ke endpoint WebSocket dan bertukar pesan secara real time."
+ },
+ "whoisLookup": {
+ "title": "Pencarian Whois",
+ "description": "Cari detail pendaftaran domain dan IP melalui RDAP."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Generator & Pemeriksa Bcrypt",
+ "subtitle": "Hash kata sandi dengan bcrypt dan verifikasi hash — semuanya tetap di browser Anda",
+ "tabs": {
+ "hash": "Hash",
+ "verify": "Verifikasi"
+ },
+ "showPassword": "Tampilkan kata sandi",
+ "hidePassword": "Sembunyikan kata sandi",
+ "copy": "Salin",
+ "copied": "Disalin",
+ "hash": {
+ "passwordLabel": "Kata sandi",
+ "passwordPlaceholder": "Masukkan kata sandi untuk di-hash…",
+ "costLabel": "Faktor biaya",
+ "costHint": "Biaya yang lebih tinggi menggandakan pekerjaan per langkah. 10-12 adalah pilihan umum untuk produksi.",
+ "slowWarning": "Faktor biaya 13 ke atas bisa memakan waktu beberapa detik untuk dihitung.",
+ "generate": "Buat hash",
+ "generating": "Meng-hash…",
+ "resultLabel": "Hash Bcrypt",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "Anatomi hash",
+ "algorithm": "algoritma",
+ "cost": "biaya",
+ "salt": "salt (22 karakter)",
+ "hash": "hash (31 karakter)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Kata sandi",
+ "passwordPlaceholder": "Masukkan kata sandi yang akan diperiksa…",
+ "hashLabel": "Hash Bcrypt",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Masukkan kata sandi dan hash bcrypt untuk membandingkannya.",
+ "invalidHash": "Ini sepertinya bukan hash bcrypt yang valid.",
+ "checking": "Membandingkan…",
+ "match": "Cocok — kata sandi ini menghasilkan hash ini.",
+ "noMatch": "Tidak cocok — kata sandi ini tidak menghasilkan hash ini."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Kalkulator Chmod",
+ "subtitle": "Konversi izin file Unix antara kotak centang, oktal, dan notasi simbolik",
+ "grid": {
+ "title": "Izin",
+ "read": "Baca",
+ "write": "Tulis",
+ "execute": "Eksekusi",
+ "owner": "Pemilik",
+ "group": "Grup",
+ "others": "Lainnya"
+ },
+ "special": {
+ "title": "Bit khusus",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "Oktal",
+ "placeholder": "755 atau 4755",
+ "invalid": "Masukkan 3 atau 4 digit oktal (0-7)."
+ },
+ "symbolic": {
+ "label": "Simbolik",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Masukkan 9 karakter seperti rwxr-xr-x (s/S, t/T diperbolehkan)."
+ },
+ "output": {
+ "title": "Perintah",
+ "copy": "Salin",
+ "copied": "Disalin"
+ },
+ "explain": {
+ "line": "{cls} dapat {perms}.",
+ "none": "{cls} tidak memiliki izin.",
+ "setuid": "Setuid: file berjalan dengan hak istimewa pemiliknya.",
+ "setgid": "Setgid: file berjalan dengan hak istimewa grupnya (file baru dalam direktori mewarisi grupnya).",
+ "sticky": "Sticky: hanya pemilik file yang dapat menghapus atau mengganti namanya di dalam direktori ini."
+ },
+ "presets": {
+ "title": "Preset umum",
+ "octal": "Oktal",
+ "permissions": "Izin",
+ "description": "Penggunaan umum",
+ "p644": "File biasa: pemilik menulis, semua orang membaca.",
+ "p755": "File eksekusi dan direktori: semua orang dapat membaca dan masuk.",
+ "p600": "File privat: hanya pemilik yang membaca dan menulis.",
+ "p700": "File eksekusi dan direktori privat: hanya pemilik.",
+ "p664": "File yang dapat ditulis grup.",
+ "p775": "Direktori dan file eksekusi yang dapat ditulis grup.",
+ "p400": "Baca saja untuk pemilik (mis. kunci privat SSH)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Tangkapan Kode",
+ "subtitle": "Ubah kode menjadi gambar indah yang mudah dibagikan",
+ "languageLabel": "Bahasa",
+ "autoDetect": "Deteksi otomatis",
+ "backgroundLabel": "Latar belakang",
+ "paddingLabel": "Padding",
+ "filenameLabel": "Nama file",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "Titik jendela",
+ "themeLabel": "Tema jendela",
+ "themeDark": "Gelap",
+ "themeLight": "Terang",
+ "scaleLabel": "Skala ekspor",
+ "copyImage": "Salin gambar",
+ "copied": "Disalin",
+ "exportPng": "Ekspor PNG",
+ "inputLabel": "Kode",
+ "detected": "Terdeteksi: {language}",
+ "clear": "Bersihkan",
+ "placeholder": "Tempel kode Anda di sini…",
+ "previewLabel": "Pratinjau",
+ "emptyPreview": "Pratinjau kode Anda akan muncul di sini",
+ "exportFailed": "Gagal mengekspor gambar",
+ "copyUnsupported": "Menyalin gambar tidak didukung di browser ini",
+ "copyFailed": "Gagal menyalin gambar",
+ "backgrounds": {
+ "ocean": "Samudra",
+ "sunset": "Senja",
+ "candy": "Permen",
+ "forest": "Hutan",
+ "midnight": "Tengah malam",
+ "gold": "Emas",
+ "aurora": "Aurora",
+ "slate": "Batu tulis",
+ "transparent": "Transparan"
+ }
+ },
+ "CssGenerators": {
+ "title": "Generator CSS",
+ "subtitle": "Box-shadow, border-radius, dan clamp() fluid — salin CSS-nya.",
+ "preview": "Pratinjau",
+ "output": "CSS",
+ "copy": "Salin",
+ "copied": "Disalin",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "clamp() fluid"
+ },
+ "boxShadow": {
+ "presets": "Preset",
+ "layer": "Lapisan",
+ "addLayer": "Tambah lapisan",
+ "removeLayer": "Hapus lapisan",
+ "x": "X",
+ "y": "Y",
+ "blur": "Blur",
+ "spread": "Sebaran",
+ "color": "Warna",
+ "alpha": "Alfa",
+ "inset": "Inset",
+ "presetNames": {
+ "subtleCard": "Kartu halus",
+ "elevated": "Terangkat",
+ "hardOffset": "Offset tegas",
+ "neonGlow": "Cahaya neon",
+ "inner": "Bayangan dalam",
+ "layeredSmooth": "Berlapis halus"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Mode",
+ "modes": {
+ "simple": "Per sudut",
+ "blob": "Blob organik"
+ },
+ "unit": "Satuan",
+ "linkCorners": "Tautkan sudut",
+ "randomize": "Acak",
+ "horizontal": "Radius horizontal",
+ "vertical": "Radius vertikal",
+ "corners": {
+ "tl": "Kiri atas",
+ "tr": "Kanan atas",
+ "br": "Kanan bawah",
+ "bl": "Kiri bawah"
+ }
+ },
+ "clamp": {
+ "minFont": "Font min (px)",
+ "maxFont": "Font maks (px)",
+ "minViewport": "Viewport min (px)",
+ "maxViewport": "Viewport maks (px)",
+ "rootFont": "Font root (px)",
+ "viewportWidth": "Viewport simulasi",
+ "computedSize": "Ukuran terhitung",
+ "slope": "Kemiringan",
+ "intercept": "Intersep",
+ "previewText": "The quick brown fox",
+ "invalid": "Masukkan batas viewport yang valid dan berbeda."
+ }
+ },
+ "ExifViewer": {
+ "title": "Penampil & Penghapus EXIF",
+ "subtitle": "Periksa metadata foto dan hapus di browser Anda",
+ "dropTitle": "Jatuhkan gambar di sini",
+ "dropHint": "atau klik untuk menjelajah — gambar Anda tidak pernah meninggalkan perangkat",
+ "browse": "Pilih gambar",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Membaca metadata…",
+ "parseFailed": "Gagal membaca gambar ini",
+ "clear": "Hapus gambar",
+ "gpsWarning": "Berisi lokasi GPS",
+ "openMap": "Lihat di OpenStreetMap",
+ "noMetadata": "Tidak ada metadata tertanam yang ditemukan pada gambar ini.",
+ "facts": {
+ "name": "Nama",
+ "type": "Tipe",
+ "dimensions": "Dimensi",
+ "size": "Ukuran"
+ },
+ "groups": {
+ "camera": "Kamera",
+ "exposure": "Pencahayaan",
+ "timestamps": "Stempel waktu",
+ "gps": "Lokasi GPS",
+ "software": "Perangkat lunak & lainnya"
+ },
+ "gpsCard": {
+ "latitude": "Lintang",
+ "longitude": "Bujur",
+ "altitude": "Ketinggian"
+ },
+ "raw": {
+ "title": "Semua metadata",
+ "copyJson": "Salin JSON",
+ "copied": "Disalin"
+ },
+ "remove": {
+ "title": "Hapus metadata",
+ "jpegDescription": "Hapus EXIF, XMP, IPTC, dan komentar tanpa kehilangan kualitas dan tanpa mengompresi ulang gambar.",
+ "reencodeDescription": "Encode ulang gambar untuk menghapus metadata tertanam. Ini mengompresi ulang piksel.",
+ "keepIcc": "Pertahankan profil warna (ICC)",
+ "stripButton": "Hapus metadata",
+ "reencodeButton": "Encode ulang & hapus",
+ "unsupported": "Penghapusan metadata tidak didukung untuk tipe file ini.",
+ "failed": "Gagal menghapus metadata",
+ "before": "Sebelum",
+ "after": "Sesudah",
+ "download": "Unduh gambar yang sudah dibersihkan"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Generator Favicon",
+ "subtitle": "Buat favicon dan ikon aplikasi dari satu gambar",
+ "dropTitle": "Jatuhkan gambar di sini",
+ "dropHint": "PNG, JPEG atau SVG — klik untuk menjelajah",
+ "browse": "Pilih gambar",
+ "clear": "Hapus gambar",
+ "backgroundLabel": "Latar belakang",
+ "transparent": "Transparan",
+ "solidColor": "Solid",
+ "cornerLabel": "Bentuk sudut",
+ "cornerNone": "Persegi",
+ "cornerRounded": "Membulat",
+ "cornerCircle": "Lingkaran",
+ "maskPaddingLabel": "Padding maskable",
+ "maskable": "maskable",
+ "previewLabel": "Pratinjau",
+ "copy": "Salin",
+ "headSnippet": "Cuplikan HTML head",
+ "downloadIco": "Unduh favicon.ico",
+ "downloadZip": "Unduh semua (.zip)"
+ },
+ "JsonDiff": {
+ "title": "Diff JSON",
+ "subtitle": "Perbandingan semantik dan sadar struktur atas dua dokumen JSON — urutan kunci dan format diabaikan",
+ "options": {
+ "ignoreArrayOrder": "Abaikan urutan array"
+ },
+ "loadBothSamples": "Muat contoh",
+ "sample": "Contoh",
+ "format": "Format",
+ "clear": "Bersihkan",
+ "formatError": "Tidak dapat memformat: input bukan JSON yang valid",
+ "placeholder": "Tempel atau ketik JSON di sini…",
+ "panels": {
+ "left": "Asli (kiri)",
+ "right": "Dimodifikasi (kanan)",
+ "results": "Perbedaan"
+ },
+ "summary": {
+ "added": "{count} ditambahkan",
+ "removed": "{count} dihapus",
+ "changed": "{count} diubah"
+ },
+ "types": {
+ "added": "Ditambahkan",
+ "removed": "Dihapus",
+ "changed": "Diubah"
+ },
+ "copyChanges": "Salin sebagai JSON",
+ "copied": "Disalin",
+ "empty": "Tempel JSON ke kedua panel untuk melihat perbedaannya.",
+ "identical": "Kedua dokumen identik secara semantik."
+ },
+ "JsonpathPlayground": {
+ "title": "Playground JSONPath",
+ "subtitle": "Kueri dokumen JSON dengan JSONPath atau JMESPath dan lihat hasilnya secara langsung",
+ "engineLabel": "Engine",
+ "queryLabel": "Kueri",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Contekan",
+ "cheats": {
+ "jpAllTitles": "Semua judul buku",
+ "jpAllAuthors": "Setiap penulis, di mana pun dalam dokumen",
+ "jpFirstBook": "Buku pertama",
+ "jpLastBook": "Buku terakhir",
+ "jpSlice": "Dua buku pertama (slice)",
+ "jpFilterPrice": "Buku lebih murah dari 40",
+ "jpFilterInStock": "Judul buku yang tersedia",
+ "jpRecursive": "Semua nilai, secara rekursif",
+ "jmAllTitles": "Semua judul buku",
+ "jmFirstAuthor": "Penulis buku pertama",
+ "jmLastBook": "Buku terakhir",
+ "jmSlice": "Dua buku pertama (slice)",
+ "jmFilterPrice": "Judul buku yang lebih murah dari 40",
+ "jmCount": "Hitung buku yang tersedia",
+ "jmProjection": "Bentuk ulang setiap buku menjadi objek baru",
+ "jmSort": "Judul diurutkan berdasarkan harga"
+ },
+ "panels": {
+ "input": "Input JSON",
+ "results": "Hasil"
+ },
+ "sample": "Contoh",
+ "format": "Format",
+ "clear": "Bersihkan",
+ "formatError": "Tidak dapat memformat: input bukan JSON yang valid",
+ "placeholder": "Tempel atau ketik JSON di sini…",
+ "outputPlaceholder": "Kecocokan akan muncul di sini…",
+ "matchCount": "{count, plural, one {# kecocokan} other {# kecocokan}}",
+ "copy": "Salin",
+ "copied": "Disalin"
+ },
+ "KeycodeInspector": {
+ "title": "Inspektur Keycode",
+ "subtitle": "Tekan tombol apa saja untuk memeriksa event keyboard JavaScript-nya",
+ "capture": {
+ "prompt": "Tekan tombol apa saja…",
+ "ready": "Mendengarkan — tekan tombol apa saja di keyboard Anda",
+ "clickToFocus": "Klik di sini untuk fokus, lalu tekan tombol apa saja"
+ },
+ "hero": {
+ "spaceKey": "(Spasi)",
+ "keyLabel": "event.key",
+ "repeat": "ulang"
+ },
+ "copyJson": "Salin JSON",
+ "copiedJson": "JSON event disalin ke clipboard",
+ "deprecated": "usang",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Standar",
+ "left": "Kiri",
+ "right": "Kanan",
+ "numpad": "Numpad",
+ "unknown": "Tidak diketahui"
+ },
+ "history": {
+ "title": "Riwayat",
+ "clear": "Bersihkan riwayat",
+ "empty": "Penekanan tombol akan muncul di sini."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Generator Tabel Markdown",
+ "subtitle": "Edit grid, tempel dari spreadsheet, dan salin tabel Markdown atau HTML yang tersejajar rapi",
+ "addRow": "Tambah baris",
+ "addColumn": "Tambah kolom",
+ "importLabel": "Impor",
+ "importPlaceholder": "Tempel TSV (dari Excel / Google Sheets) atau CSV di sini…",
+ "importButton": "Impor ke grid",
+ "pasteClipboard": "Tempel dari clipboard",
+ "importEmpty": "Tidak ada yang diimpor — tempel beberapa baris dulu",
+ "clipboardError": "Tidak dapat membaca dari clipboard",
+ "clear": "Bersihkan",
+ "panels": {
+ "editor": "Editor tabel",
+ "output": "Output"
+ },
+ "align": {
+ "left": "Rata kiri",
+ "center": "Rata tengah",
+ "right": "Rata kanan"
+ },
+ "headerPlaceholder": "Header",
+ "cellPlaceholder": "Sel",
+ "removeRow": "Hapus baris",
+ "removeColumn": "Hapus kolom",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Salin",
+ "copied": "Disalin"
+ },
+ "TimezoneConverter": {
+ "title": "Konverter Zona Waktu",
+ "subtitle": "Konversi satu momen ke seluruh dunia untuk merencanakan rapat.",
+ "dateTimeLabel": "Tanggal & waktu",
+ "now": "Sekarang",
+ "sourceZoneLabel": "Zona waktu sumber",
+ "selectZone": "Pilih zona",
+ "searchZone": "Cari zona waktu...",
+ "noZone": "Zona waktu tidak ditemukan.",
+ "scrubLabel": "Geser waktu",
+ "reset": "Atur ulang",
+ "targetsLabel": "Zona tujuan",
+ "addZone": "Tambah zona",
+ "remove": "Hapus",
+ "localBadge": "Lokal",
+ "invalidDate": "Masukkan tanggal dan waktu yang valid.",
+ "bands": {
+ "work": "Jam kerja (09:00–18:00)",
+ "edge": "Jam pinggir (pagi / malam)",
+ "night": "Malam"
+ }
+ },
+ "TokenCounter": {
+ "title": "Penghitung Token LLM",
+ "subtitle": "Hitung token, visualisasikan potongan, dan perkirakan biaya API.",
+ "inputLabel": "Teks",
+ "sample": "Contoh",
+ "clear": "Bersihkan",
+ "placeholder": "Tempel teks, kode, atau JSON untuk menghitung token...",
+ "encoding": "Encoding",
+ "loading": "Memuat tokenizer...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / seri o)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Token",
+ "characters": "Karakter",
+ "words": "Kata",
+ "ratio": "Token/kata",
+ "ratioSub": "kira-kira"
+ },
+ "claudeRow": "Claude (perkiraan)",
+ "estimate": "perkiraan",
+ "viz": {
+ "title": "Visualisasi token",
+ "toggle": "Tampilkan",
+ "hint": "Alihkan untuk mewarnai setiap token sebagai satu potongan.",
+ "empty": "Masukkan teks untuk memvisualisasikan token.",
+ "truncated": "Menampilkan {limit} token pertama dari {total}."
+ },
+ "cost": {
+ "title": "Kalkulator biaya",
+ "disclaimer": "nilai bawaan dapat diedit — verifikasi harga terkini",
+ "model": "Model",
+ "expectedOutput": "Token output yang diharapkan",
+ "customInput": "Input $/MTok",
+ "customOutput": "Output $/MTok",
+ "inputCost": "Biaya input",
+ "outputCost": "Biaya output",
+ "total": "Total",
+ "per1000": "Per 1000 panggilan"
+ }
+ },
+ "WebhookTester": {
+ "title": "Penguji Webhook",
+ "subtitle": "Buat URL sementara dan periksa permintaan webhook masuk secara langsung.",
+ "intro": {
+ "title": "Buat URL webhook untuk mulai menangkap permintaan",
+ "hint": "Anda akan mendapat URL yang tidak dapat ditebak yang merekam setiap permintaan masuk. URL kedaluwarsa setelah 24 jam."
+ },
+ "createBtn": "Buat URL webhook",
+ "creating": "Membuat…",
+ "urlLabel": "URL webhook Anda",
+ "expiresIn": "Kedaluwarsa dalam {countdown}",
+ "copyUrlBtn": "Salin URL",
+ "copied": "Disalin",
+ "clearBtn": "Bersihkan",
+ "newBinBtn": "URL baru",
+ "requestsTitle": "Permintaan",
+ "selectPrompt": "Pilih permintaan untuk memeriksa detailnya.",
+ "expiredNotice": "URL webhook ini telah kedaluwarsa. Buat yang baru untuk terus menguji.",
+ "base64Badge": "base64",
+ "truncatedBadge": "dipotong",
+ "empty": {
+ "title": "Menunggu permintaan…",
+ "hint": "Kirim permintaan ke URL Anda dan itu akan muncul di sini seketika. Coba ini:"
+ },
+ "detail": {
+ "receivedAt": "Diterima",
+ "source": "IP sumber",
+ "contentType": "Content-Type",
+ "query": "Parameter kueri",
+ "headers": "Header",
+ "body": "Body",
+ "copyBody": "Salin body",
+ "emptyBody": "Tidak ada body"
+ },
+ "errors": {
+ "createFailed": "Gagal membuat URL webhook. Silakan coba lagi.",
+ "clearFailed": "Gagal membersihkan permintaan. Silakan coba lagi."
+ }
+ },
+ "WebsocketTester": {
+ "title": "Penguji WebSocket",
+ "subtitle": "Sambungkan ke endpoint WebSocket lalu kirim, terima, dan periksa pesan secara langsung",
+ "urlLabel": "URL endpoint",
+ "protocolsLabel": "Subprotokol",
+ "protocolsPlaceholder": "opsional, dipisahkan koma",
+ "connect": "Hubungkan",
+ "disconnect": "Putuskan",
+ "status": {
+ "disconnected": "Terputus",
+ "connecting": "Menghubungkan",
+ "connected": "Terhubung",
+ "error": "Kesalahan"
+ },
+ "messages": {
+ "connecting": "Menghubungkan ke {url}…",
+ "connected": "Koneksi berhasil dibuat",
+ "connectedWithProtocol": "Koneksi berhasil dibuat (protokol: {protocol})",
+ "invalidUrl": "URL tidak valid — harus dimulai dengan ws:// atau wss://",
+ "connectFailed": "Gagal membuka koneksi",
+ "socketError": "Terjadi kesalahan koneksi",
+ "notConnected": "Tidak terhubung — hubungkan dulu",
+ "binaryMessage": "Menerima pesan biner"
+ },
+ "log": {
+ "title": "Log Pesan",
+ "empty": "Belum ada pesan. Hubungkan dan mulai mengirim.",
+ "copyAll": "Salin semua",
+ "download": "Unduh sebagai .txt",
+ "clear": "Bersihkan log",
+ "prettyPrint": "Cetak rapi JSON",
+ "showRaw": "Tampilkan teks mentah"
+ },
+ "composer": {
+ "placeholder": "Ketik pesan untuk dikirim…",
+ "send": "Kirim",
+ "formatJson": "Format sebagai JSON",
+ "invalidJson": "Pesan bukan JSON yang valid",
+ "hint": "Cmd/Ctrl+Enter untuk mengirim"
+ },
+ "ping": {
+ "label": "Ping otomatis",
+ "seconds": "dtk",
+ "intervalTitle": "Interval ping dalam detik",
+ "payloadPlaceholder": "payload ping"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Pencarian Whois",
+ "subtitle": "Cari detail pendaftaran untuk domain atau alamat IP apa pun melalui RDAP",
+ "placeholder": "example.com atau 8.8.8.8",
+ "lookupBtn": "Cari",
+ "looking": "Mencari…",
+ "refreshBtn": "Segarkan",
+ "tryLabel": "Coba:",
+ "copy": "Salin",
+ "copied": "Disalin",
+ "notAvailable": "Tidak tersedia",
+ "summary": {
+ "registrar": "Registrar",
+ "network": "Jaringan"
+ },
+ "dates": {
+ "created": "Dibuat",
+ "updated": "Diperbarui",
+ "expires": "Kedaluwarsa"
+ },
+ "sections": {
+ "status": "Status",
+ "nameservers": "Nameserver",
+ "contacts": "Kontak",
+ "rawRdap": "RDAP mentah"
+ },
+ "contacts": {
+ "roles": "Peran",
+ "name": "Nama",
+ "organization": "Organisasi"
+ },
+ "errors": {
+ "invalidTarget": "Masukkan nama domain atau alamat IP yang valid",
+ "lookupFailed": "Pencarian gagal (status {status})"
+ }
}
}
diff --git a/apps/web/messages/it.json b/apps/web/messages/it.json
index 4e95fc93..c94ecb52 100644
--- a/apps/web/messages/it.json
+++ b/apps/web/messages/it.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Calcolatore Chmod",
+ "codeScreenshot": "Screenshot di codice",
+ "cssGenerators": "Generatori CSS",
+ "exifViewer": "Visualizzatore EXIF",
+ "faviconGenerator": "Generatore di favicon",
+ "jsonDiff": "Diff JSON",
+ "jsonpathPlayground": "Playground JSONPath",
+ "keycodeInspector": "Ispettore di keycode",
+ "markdownTableGenerator": "Tabella Markdown",
+ "timezoneConverter": "Convertitore di fusi orari",
+ "tokenCounter": "Contatore di token",
+ "webhookTester": "Tester di webhook",
+ "websocketTester": "Tester WebSocket",
+ "whoisLookup": "Ricerca Whois"
},
"Help": {
"title": "Aiuto e documentazione",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Generatore e verificatore Bcrypt",
+ "description": "Genera hash di password con bcrypt e verifica hash esistenti."
+ },
+ "chmodCalculator": {
+ "title": "Calcolatore Chmod",
+ "description": "Converti i permessi Unix tra ottale, simbolico e caselle di controllo."
+ },
+ "codeScreenshot": {
+ "title": "Screenshot di codice",
+ "description": "Trasforma il codice sorgente in immagini eleganti e condivisibili."
+ },
+ "cssGenerators": {
+ "title": "Generatori CSS",
+ "description": "Generatori di box-shadow, border-radius e clamp() fluido."
+ },
+ "exifViewer": {
+ "title": "Visualizzatore e rimozione EXIF",
+ "description": "Ispeziona e rimuovi i metadati EXIF delle foto in locale."
+ },
+ "faviconGenerator": {
+ "title": "Generatore di favicon",
+ "description": "Genera favicon, icone dell'app e snippet di manifest."
+ },
+ "jsonDiff": {
+ "title": "Diff JSON",
+ "description": "Confronto semantico e consapevole della struttura tra due documenti JSON."
+ },
+ "jsonpathPlayground": {
+ "title": "Playground JSONPath",
+ "description": "Interroga JSON con JSONPath o JMESPath e vedi i risultati in tempo reale."
+ },
+ "keycodeInspector": {
+ "title": "Ispettore di keycode",
+ "description": "Premi un tasto per vedere i suoi codici evento JavaScript e i modificatori."
+ },
+ "markdownTableGenerator": {
+ "title": "Generatore di tabelle Markdown",
+ "description": "Crea tabelle Markdown e HTML in un editor a griglia simile a un foglio di calcolo."
+ },
+ "timezoneConverter": {
+ "title": "Convertitore di fusi orari",
+ "description": "Converti un orario tra molti fusi con indicazioni di cambio giorno e orario lavorativo."
+ },
+ "tokenCounter": {
+ "title": "Contatore di token LLM",
+ "description": "Conta i token LLM, visualizza i chunk e stima il costo dell'API."
+ },
+ "webhookTester": {
+ "title": "Tester di webhook",
+ "description": "Cattura e ispeziona le richieste webhook in arrivo in tempo reale."
+ },
+ "websocketTester": {
+ "title": "Tester WebSocket",
+ "description": "Connettiti a endpoint WebSocket e scambia messaggi in tempo reale."
+ },
+ "whoisLookup": {
+ "title": "Ricerca Whois",
+ "description": "Cerca i dettagli di registrazione di domini e IP tramite RDAP."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Generatore e verificatore Bcrypt",
+ "subtitle": "Genera hash di password con bcrypt e verifica gli hash — tutto resta nel tuo browser",
+ "tabs": {
+ "hash": "Hash",
+ "verify": "Verifica"
+ },
+ "showPassword": "Mostra password",
+ "hidePassword": "Nascondi password",
+ "copy": "Copia",
+ "copied": "Copiato",
+ "hash": {
+ "passwordLabel": "Password",
+ "passwordPlaceholder": "Inserisci una password da hashare…",
+ "costLabel": "Fattore di costo",
+ "costHint": "Un costo più alto raddoppia il lavoro per passaggio. 10-12 è una scelta comune in produzione.",
+ "slowWarning": "I fattori di costo pari o superiori a 13 possono richiedere diversi secondi per il calcolo.",
+ "generate": "Genera hash",
+ "generating": "Hashing…",
+ "resultLabel": "Hash bcrypt",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "Anatomia dell'hash",
+ "algorithm": "algoritmo",
+ "cost": "costo",
+ "salt": "salt (22 caratteri)",
+ "hash": "hash (31 caratteri)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Password",
+ "passwordPlaceholder": "Inserisci la password da verificare…",
+ "hashLabel": "Hash bcrypt",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Inserisci una password e un hash bcrypt per confrontarli.",
+ "invalidHash": "Questo non sembra un hash bcrypt valido.",
+ "checking": "Confronto in corso…",
+ "match": "Corrispondenza — la password produce questo hash.",
+ "noMatch": "Nessuna corrispondenza — la password non produce questo hash."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Calcolatore Chmod",
+ "subtitle": "Converti i permessi dei file Unix tra caselle di controllo, ottale e notazione simbolica",
+ "grid": {
+ "title": "Permessi",
+ "read": "Lettura",
+ "write": "Scrittura",
+ "execute": "Esecuzione",
+ "owner": "Proprietario",
+ "group": "Gruppo",
+ "others": "Altri"
+ },
+ "special": {
+ "title": "Bit speciali",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "Ottale",
+ "placeholder": "755 o 4755",
+ "invalid": "Inserisci 3 o 4 cifre ottali (0-7)."
+ },
+ "symbolic": {
+ "label": "Simbolico",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Inserisci 9 caratteri come rwxr-xr-x (sono consentiti s/S, t/T)."
+ },
+ "output": {
+ "title": "Comando",
+ "copy": "Copia",
+ "copied": "Copiato"
+ },
+ "explain": {
+ "line": "{cls} può {perms}.",
+ "none": "{cls} non ha permessi.",
+ "setuid": "Setuid: il file viene eseguito con i privilegi del proprietario.",
+ "setgid": "Setgid: il file viene eseguito con i privilegi del suo gruppo (i nuovi file in una directory ne ereditano il gruppo).",
+ "sticky": "Sticky: solo il proprietario di un file può eliminarlo o rinominarlo all'interno di questa directory."
+ },
+ "presets": {
+ "title": "Preimpostazioni comuni",
+ "octal": "Ottale",
+ "permissions": "Permessi",
+ "description": "Uso tipico",
+ "p644": "File normali: il proprietario scrive, tutti leggono.",
+ "p755": "Eseguibili e directory: tutti possono leggere ed entrare.",
+ "p600": "File privati: solo il proprietario legge e scrive.",
+ "p700": "Eseguibili e directory privati: solo il proprietario.",
+ "p664": "File scrivibili dal gruppo.",
+ "p775": "Directory ed eseguibili scrivibili dal gruppo.",
+ "p400": "Sola lettura per il proprietario (es. chiavi private SSH)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Screenshot di codice",
+ "subtitle": "Trasforma il codice in immagini eleganti e condivisibili",
+ "languageLabel": "Linguaggio",
+ "autoDetect": "Rilevamento automatico",
+ "backgroundLabel": "Sfondo",
+ "paddingLabel": "Spaziatura",
+ "filenameLabel": "Nome del file",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "Puntini della finestra",
+ "themeLabel": "Tema della finestra",
+ "themeDark": "Scuro",
+ "themeLight": "Chiaro",
+ "scaleLabel": "Scala di esportazione",
+ "copyImage": "Copia immagine",
+ "copied": "Copiato",
+ "exportPng": "Esporta PNG",
+ "inputLabel": "Codice",
+ "detected": "Rilevato: {language}",
+ "clear": "Pulisci",
+ "placeholder": "Incolla qui il tuo codice…",
+ "previewLabel": "Anteprima",
+ "emptyPreview": "L'anteprima del tuo codice apparirà qui",
+ "exportFailed": "Esportazione dell'immagine non riuscita",
+ "copyUnsupported": "La copia delle immagini non è supportata in questo browser",
+ "copyFailed": "Copia dell'immagine non riuscita",
+ "backgrounds": {
+ "ocean": "Oceano",
+ "sunset": "Tramonto",
+ "candy": "Caramella",
+ "forest": "Foresta",
+ "midnight": "Mezzanotte",
+ "gold": "Oro",
+ "aurora": "Aurora",
+ "slate": "Ardesia",
+ "transparent": "Trasparente"
+ }
+ },
+ "CssGenerators": {
+ "title": "Generatori CSS",
+ "subtitle": "Box-shadow, border-radius e clamp() fluido — copia il CSS.",
+ "preview": "Anteprima",
+ "output": "CSS",
+ "copy": "Copia",
+ "copied": "Copiato",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "clamp() fluido"
+ },
+ "boxShadow": {
+ "presets": "Preimpostazioni",
+ "layer": "Livello",
+ "addLayer": "Aggiungi livello",
+ "removeLayer": "Rimuovi livello",
+ "x": "X",
+ "y": "Y",
+ "blur": "Sfocatura",
+ "spread": "Estensione",
+ "color": "Colore",
+ "alpha": "Alfa",
+ "inset": "Interno",
+ "presetNames": {
+ "subtleCard": "Scheda delicata",
+ "elevated": "Rialzato",
+ "hardOffset": "Offset netto",
+ "neonGlow": "Bagliore neon",
+ "inner": "Interno",
+ "layeredSmooth": "Stratificato morbido"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Modalità",
+ "modes": {
+ "simple": "Per angolo",
+ "blob": "Blob organico"
+ },
+ "unit": "Unità",
+ "linkCorners": "Collega gli angoli",
+ "randomize": "Casuale",
+ "horizontal": "Raggi orizzontali",
+ "vertical": "Raggi verticali",
+ "corners": {
+ "tl": "In alto a sinistra",
+ "tr": "In alto a destra",
+ "br": "In basso a destra",
+ "bl": "In basso a sinistra"
+ }
+ },
+ "clamp": {
+ "minFont": "Font min (px)",
+ "maxFont": "Font max (px)",
+ "minViewport": "Viewport min (px)",
+ "maxViewport": "Viewport max (px)",
+ "rootFont": "Font radice (px)",
+ "viewportWidth": "Viewport simulato",
+ "computedSize": "Dimensione calcolata",
+ "slope": "Pendenza",
+ "intercept": "Intercetta",
+ "previewText": "Ma la volpe col suo balzo",
+ "invalid": "Inserisci limiti di viewport validi e diversi tra loro."
+ }
+ },
+ "ExifViewer": {
+ "title": "Visualizzatore e rimozione EXIF",
+ "subtitle": "Ispeziona i metadati delle foto e rimuovili nel tuo browser",
+ "dropTitle": "Trascina qui un'immagine",
+ "dropHint": "oppure clicca per sfogliare — la tua immagine non lascia mai il tuo dispositivo",
+ "browse": "Scegli immagine",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Lettura dei metadati…",
+ "parseFailed": "Impossibile leggere questa immagine",
+ "clear": "Rimuovi immagine",
+ "gpsWarning": "Contiene posizione GPS",
+ "openMap": "Visualizza su OpenStreetMap",
+ "noMetadata": "Nessun metadato incorporato trovato in questa immagine.",
+ "facts": {
+ "name": "Nome",
+ "type": "Tipo",
+ "dimensions": "Dimensioni",
+ "size": "Peso"
+ },
+ "groups": {
+ "camera": "Fotocamera",
+ "exposure": "Esposizione",
+ "timestamps": "Timestamp",
+ "gps": "Posizione GPS",
+ "software": "Software e altro"
+ },
+ "gpsCard": {
+ "latitude": "Latitudine",
+ "longitude": "Longitudine",
+ "altitude": "Altitudine"
+ },
+ "raw": {
+ "title": "Tutti i metadati",
+ "copyJson": "Copia JSON",
+ "copied": "Copiato"
+ },
+ "remove": {
+ "title": "Rimuovi metadati",
+ "jpegDescription": "Rimuovi senza perdita EXIF, XMP, IPTC e commenti senza ricomprimere l'immagine.",
+ "reencodeDescription": "Ricodifica l'immagine per rimuovere i metadati incorporati. Questo ricomprime i pixel.",
+ "keepIcc": "Mantieni il profilo colore (ICC)",
+ "stripButton": "Rimuovi metadati",
+ "reencodeButton": "Ricodifica e rimuovi",
+ "unsupported": "La rimozione dei metadati non è supportata per questo tipo di file.",
+ "failed": "Impossibile rimuovere i metadati",
+ "before": "Prima",
+ "after": "Dopo",
+ "download": "Scarica immagine ripulita"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Generatore di favicon",
+ "subtitle": "Crea favicon e icone dell'app da una singola immagine",
+ "dropTitle": "Trascina qui un'immagine",
+ "dropHint": "PNG, JPEG o SVG — clicca per sfogliare",
+ "browse": "Scegli immagine",
+ "clear": "Rimuovi immagine",
+ "backgroundLabel": "Sfondo",
+ "transparent": "Trasparente",
+ "solidColor": "Tinta unita",
+ "cornerLabel": "Forma degli angoli",
+ "cornerNone": "Quadrato",
+ "cornerRounded": "Arrotondato",
+ "cornerCircle": "Cerchio",
+ "maskPaddingLabel": "Spaziatura maskable",
+ "maskable": "maskable",
+ "previewLabel": "Anteprima",
+ "copy": "Copia",
+ "headSnippet": "Snippet per l'head HTML",
+ "downloadIco": "Scarica favicon.ico",
+ "downloadZip": "Scarica tutto (.zip)"
+ },
+ "JsonDiff": {
+ "title": "Diff JSON",
+ "subtitle": "Confronto semantico e consapevole della struttura tra due documenti JSON — ordine delle chiavi e formattazione ignorati",
+ "options": {
+ "ignoreArrayOrder": "Ignora l'ordine degli array"
+ },
+ "loadBothSamples": "Carica esempi",
+ "sample": "Carica esempio",
+ "format": "Formatta",
+ "clear": "Pulisci",
+ "formatError": "Impossibile formattare: l'input non è un JSON valido",
+ "placeholder": "Incolla o digita qui il JSON…",
+ "panels": {
+ "left": "Originale (sinistra)",
+ "right": "Modificato (destra)",
+ "results": "Differenze"
+ },
+ "summary": {
+ "added": "{count} aggiunti",
+ "removed": "{count} rimossi",
+ "changed": "{count} modificati"
+ },
+ "types": {
+ "added": "Aggiunto",
+ "removed": "Rimosso",
+ "changed": "Modificato"
+ },
+ "copyChanges": "Copia come JSON",
+ "copied": "Copiato",
+ "empty": "Incolla il JSON in entrambi i pannelli per vederne le differenze.",
+ "identical": "I due documenti sono semanticamente identici."
+ },
+ "JsonpathPlayground": {
+ "title": "Playground JSONPath",
+ "subtitle": "Interroga documenti JSON con JSONPath o JMESPath e vedi i risultati in tempo reale",
+ "engineLabel": "Motore",
+ "queryLabel": "Query",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Prontuario",
+ "cheats": {
+ "jpAllTitles": "Tutti i titoli dei libri",
+ "jpAllAuthors": "Ogni autore, ovunque nel documento",
+ "jpFirstBook": "Primo libro",
+ "jpLastBook": "Ultimo libro",
+ "jpSlice": "I primi due libri (slice)",
+ "jpFilterPrice": "Libri sotto i 40",
+ "jpFilterInStock": "Titoli dei libri disponibili",
+ "jpRecursive": "Tutti i valori, ricorsivamente",
+ "jmAllTitles": "Tutti i titoli dei libri",
+ "jmFirstAuthor": "Autore del primo libro",
+ "jmLastBook": "Ultimo libro",
+ "jmSlice": "I primi due libri (slice)",
+ "jmFilterPrice": "Titoli dei libri sotto i 40",
+ "jmCount": "Conta i libri disponibili",
+ "jmProjection": "Rimodella ogni libro in un nuovo oggetto",
+ "jmSort": "Titoli ordinati per prezzo"
+ },
+ "panels": {
+ "input": "Input JSON",
+ "results": "Risultati"
+ },
+ "sample": "Carica esempio",
+ "format": "Formatta",
+ "clear": "Pulisci",
+ "formatError": "Impossibile formattare: l'input non è un JSON valido",
+ "placeholder": "Incolla o digita qui il JSON…",
+ "outputPlaceholder": "Le corrispondenze appariranno qui…",
+ "matchCount": "{count, plural, one {# corrispondenza} other {# corrispondenze}}",
+ "copy": "Copia",
+ "copied": "Copiato"
+ },
+ "KeycodeInspector": {
+ "title": "Ispettore di keycode",
+ "subtitle": "Premi un tasto qualsiasi per ispezionare il suo evento da tastiera JavaScript",
+ "capture": {
+ "prompt": "Premi un tasto qualsiasi…",
+ "ready": "In ascolto — premi un tasto qualsiasi sulla tastiera",
+ "clickToFocus": "Clicca qui per mettere a fuoco, poi premi un tasto qualsiasi"
+ },
+ "hero": {
+ "spaceKey": "(Spazio)",
+ "keyLabel": "event.key",
+ "repeat": "ripetizione"
+ },
+ "copyJson": "Copia JSON",
+ "copiedJson": "JSON dell'evento copiato negli appunti",
+ "deprecated": "deprecato",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Standard",
+ "left": "Sinistra",
+ "right": "Destra",
+ "numpad": "Tastierino numerico",
+ "unknown": "Sconosciuto"
+ },
+ "history": {
+ "title": "Cronologia",
+ "clear": "Cancella cronologia",
+ "empty": "Le pressioni dei tasti appariranno qui."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Generatore di tabelle Markdown",
+ "subtitle": "Modifica una griglia, incolla da un foglio di calcolo e copia una tabella Markdown o HTML ben allineata",
+ "addRow": "Aggiungi riga",
+ "addColumn": "Aggiungi colonna",
+ "importLabel": "Importa",
+ "importPlaceholder": "Incolla qui TSV (da Excel / Google Sheets) o CSV…",
+ "importButton": "Importa nella griglia",
+ "pasteClipboard": "Incolla dagli appunti",
+ "importEmpty": "Niente da importare — incolla prima alcune righe",
+ "clipboardError": "Impossibile leggere dagli appunti",
+ "clear": "Pulisci",
+ "panels": {
+ "editor": "Editor di tabelle",
+ "output": "Output"
+ },
+ "align": {
+ "left": "Allinea a sinistra",
+ "center": "Centra",
+ "right": "Allinea a destra"
+ },
+ "headerPlaceholder": "Intestazione",
+ "cellPlaceholder": "Cella",
+ "removeRow": "Rimuovi riga",
+ "removeColumn": "Rimuovi colonna",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Copia",
+ "copied": "Copiato"
+ },
+ "TimezoneConverter": {
+ "title": "Convertitore di fusi orari",
+ "subtitle": "Converti un singolo momento in tutto il mondo per pianificare le riunioni.",
+ "dateTimeLabel": "Data e ora",
+ "now": "Adesso",
+ "sourceZoneLabel": "Fuso orario di origine",
+ "selectZone": "Seleziona fuso",
+ "searchZone": "Cerca fusi orari...",
+ "noZone": "Nessun fuso orario trovato.",
+ "scrubLabel": "Sposta l'ora",
+ "reset": "Reimposta",
+ "targetsLabel": "Fusi di destinazione",
+ "addZone": "Aggiungi fuso",
+ "remove": "Rimuovi",
+ "localBadge": "Locale",
+ "invalidDate": "Inserisci una data e un'ora valide.",
+ "bands": {
+ "work": "Orario lavorativo (09:00–18:00)",
+ "edge": "Orari limite (presto / sera)",
+ "night": "Notte"
+ }
+ },
+ "TokenCounter": {
+ "title": "Contatore di token LLM",
+ "subtitle": "Conta i token, visualizza i chunk e stima il costo dell'API.",
+ "inputLabel": "Testo",
+ "sample": "Carica esempio",
+ "clear": "Pulisci",
+ "placeholder": "Incolla testo, codice o JSON per contare i token...",
+ "encoding": "Codifica",
+ "loading": "Caricamento del tokenizer...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / serie o)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Token",
+ "characters": "Caratteri",
+ "words": "Parole",
+ "ratio": "Token/parola",
+ "ratioSub": "circa"
+ },
+ "claudeRow": "Claude (approssimativo)",
+ "estimate": "stima",
+ "viz": {
+ "title": "Visualizzazione dei token",
+ "toggle": "Mostra",
+ "hint": "Attiva per colorare ogni token come un chunk.",
+ "empty": "Inserisci del testo per visualizzare i token.",
+ "truncated": "Mostra i primi {limit} di {total} token."
+ },
+ "cost": {
+ "title": "Calcolatore di costi",
+ "disclaimer": "valori predefiniti modificabili — verifica i prezzi attuali",
+ "model": "Modello",
+ "expectedOutput": "Token di output previsti",
+ "customInput": "Input $/MTok",
+ "customOutput": "Output $/MTok",
+ "inputCost": "Costo di input",
+ "outputCost": "Costo di output",
+ "total": "Totale",
+ "per1000": "Ogni 1000 chiamate"
+ }
+ },
+ "WebhookTester": {
+ "title": "Tester di webhook",
+ "subtitle": "Crea un URL temporaneo e ispeziona le richieste webhook in arrivo dal vivo.",
+ "intro": {
+ "title": "Crea un URL webhook per iniziare a catturare le richieste",
+ "hint": "Otterrai un URL non indovinabile che registra ogni richiesta in arrivo. Scade dopo 24 ore."
+ },
+ "createBtn": "Crea URL webhook",
+ "creating": "Creazione…",
+ "urlLabel": "Il tuo URL webhook",
+ "expiresIn": "Scade tra {countdown}",
+ "copyUrlBtn": "Copia URL",
+ "copied": "Copiato",
+ "clearBtn": "Pulisci",
+ "newBinBtn": "Nuovo URL",
+ "requestsTitle": "Richieste",
+ "selectPrompt": "Seleziona una richiesta per ispezionarne i dettagli.",
+ "expiredNotice": "Questo URL webhook è scaduto. Creane uno nuovo per continuare a testare.",
+ "base64Badge": "base64",
+ "truncatedBadge": "troncato",
+ "empty": {
+ "title": "In attesa di richieste…",
+ "hint": "Invia una richiesta al tuo URL e apparirà qui all'istante. Prova così:"
+ },
+ "detail": {
+ "receivedAt": "Ricevuto",
+ "source": "IP di origine",
+ "contentType": "Content-Type",
+ "query": "Parametri query",
+ "headers": "Intestazioni",
+ "body": "Corpo",
+ "copyBody": "Copia corpo",
+ "emptyBody": "Nessun corpo"
+ },
+ "errors": {
+ "createFailed": "Impossibile creare l'URL webhook. Riprova.",
+ "clearFailed": "Impossibile cancellare le richieste. Riprova."
+ }
+ },
+ "WebsocketTester": {
+ "title": "Tester WebSocket",
+ "subtitle": "Connettiti a un endpoint WebSocket e invia, ricevi e ispeziona messaggi dal vivo",
+ "urlLabel": "URL dell'endpoint",
+ "protocolsLabel": "Sottoprotocolli",
+ "protocolsPlaceholder": "opzionale, separati da virgola",
+ "connect": "Connetti",
+ "disconnect": "Disconnetti",
+ "status": {
+ "disconnected": "Disconnesso",
+ "connecting": "Connessione",
+ "connected": "Connesso",
+ "error": "Errore"
+ },
+ "messages": {
+ "connecting": "Connessione a {url}…",
+ "connected": "Connessione stabilita",
+ "connectedWithProtocol": "Connessione stabilita (protocollo: {protocol})",
+ "invalidUrl": "URL non valido — deve iniziare con ws:// o wss://",
+ "connectFailed": "Impossibile aprire la connessione",
+ "socketError": "Si è verificato un errore di connessione",
+ "notConnected": "Non connesso — connettiti prima",
+ "binaryMessage": "Ricevuto un messaggio binario"
+ },
+ "log": {
+ "title": "Registro dei messaggi",
+ "empty": "Ancora nessun messaggio. Connettiti e inizia a inviare.",
+ "copyAll": "Copia tutto",
+ "download": "Scarica come .txt",
+ "clear": "Cancella registro",
+ "prettyPrint": "Formatta JSON",
+ "showRaw": "Mostra testo grezzo"
+ },
+ "composer": {
+ "placeholder": "Digita un messaggio da inviare…",
+ "send": "Invia",
+ "formatJson": "Formatta come JSON",
+ "invalidJson": "Il messaggio non è un JSON valido",
+ "hint": "Cmd/Ctrl+Invio per inviare"
+ },
+ "ping": {
+ "label": "Auto-ping",
+ "seconds": "sec",
+ "intervalTitle": "Intervallo di ping in secondi",
+ "payloadPlaceholder": "payload del ping"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Ricerca Whois",
+ "subtitle": "Cerca i dettagli di registrazione di qualsiasi dominio o indirizzo IP tramite RDAP",
+ "placeholder": "example.com o 8.8.8.8",
+ "lookupBtn": "Cerca",
+ "looking": "Ricerca…",
+ "refreshBtn": "Aggiorna",
+ "tryLabel": "Prova:",
+ "copy": "Copia",
+ "copied": "Copiato",
+ "notAvailable": "Non disponibile",
+ "summary": {
+ "registrar": "Registrar",
+ "network": "Rete"
+ },
+ "dates": {
+ "created": "Creato",
+ "updated": "Aggiornato",
+ "expires": "Scade"
+ },
+ "sections": {
+ "status": "Stato",
+ "nameservers": "Nameserver",
+ "contacts": "Contatti",
+ "rawRdap": "RDAP grezzo"
+ },
+ "contacts": {
+ "roles": "Ruoli",
+ "name": "Nome",
+ "organization": "Organizzazione"
+ },
+ "errors": {
+ "invalidTarget": "Inserisci un nome di dominio o un indirizzo IP valido",
+ "lookupFailed": "Ricerca non riuscita (stato {status})"
+ }
}
}
diff --git a/apps/web/messages/ja.json b/apps/web/messages/ja.json
index dee40c60..ba449841 100644
--- a/apps/web/messages/ja.json
+++ b/apps/web/messages/ja.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Chmod 計算機",
+ "codeScreenshot": "コードスクリーンショット",
+ "cssGenerators": "CSS ジェネレーター",
+ "exifViewer": "EXIF ビューアー",
+ "faviconGenerator": "Favicon ジェネレーター",
+ "jsonDiff": "JSON 差分",
+ "jsonpathPlayground": "JSONPath プレイグラウンド",
+ "keycodeInspector": "キーコードインスペクター",
+ "markdownTableGenerator": "Markdown テーブル",
+ "timezoneConverter": "タイムゾーン変換",
+ "tokenCounter": "トークンカウンター",
+ "webhookTester": "Webhook テスター",
+ "websocketTester": "WebSocket テスター",
+ "whoisLookup": "Whois 検索"
},
"Help": {
"title": "ヘルプとドキュメント",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Bcrypt 生成・検証",
+ "description": "bcrypt でパスワードをハッシュ化し、既存のハッシュを検証します。"
+ },
+ "chmodCalculator": {
+ "title": "Chmod 計算機",
+ "description": "Unix のパーミッションを 8 進数、シンボリック表記、チェックボックス間で変換します。"
+ },
+ "codeScreenshot": {
+ "title": "コードスクリーンショット",
+ "description": "ソースコードを美しく共有しやすい画像に変換します。"
+ },
+ "cssGenerators": {
+ "title": "CSS ジェネレーター",
+ "description": "Box-shadow、border-radius、流動的な clamp() のジェネレーター。"
+ },
+ "exifViewer": {
+ "title": "EXIF ビューアー・除去",
+ "description": "写真の EXIF メタデータをローカルで確認・除去します。"
+ },
+ "faviconGenerator": {
+ "title": "Favicon ジェネレーター",
+ "description": "favicon、アプリアイコン、マニフェストのスニペットを生成します。"
+ },
+ "jsonDiff": {
+ "title": "JSON 差分",
+ "description": "2 つの JSON ドキュメントを意味的・構造的に比較します。"
+ },
+ "jsonpathPlayground": {
+ "title": "JSONPath プレイグラウンド",
+ "description": "JSONPath または JMESPath で JSON をクエリし、結果をリアルタイムで確認します。"
+ },
+ "keycodeInspector": {
+ "title": "キーコードインスペクター",
+ "description": "キーを押して JavaScript のイベントコードと修飾キーを確認します。"
+ },
+ "markdownTableGenerator": {
+ "title": "Markdown テーブルジェネレーター",
+ "description": "スプレッドシートのようなグリッドエディターで Markdown と HTML のテーブルを作成します。"
+ },
+ "timezoneConverter": {
+ "title": "タイムゾーン変換",
+ "description": "1 つの時刻を複数のタイムゾーンに変換し、日付のずれや勤務時間を表示します。"
+ },
+ "tokenCounter": {
+ "title": "LLM トークンカウンター",
+ "description": "LLM のトークンを数え、チャンクを可視化し、API コストを見積もります。"
+ },
+ "webhookTester": {
+ "title": "Webhook テスター",
+ "description": "受信した webhook リクエストをリアルタイムでキャプチャして確認します。"
+ },
+ "websocketTester": {
+ "title": "WebSocket テスター",
+ "description": "WebSocket エンドポイントに接続し、メッセージをリアルタイムでやり取りします。"
+ },
+ "whoisLookup": {
+ "title": "Whois 検索",
+ "description": "RDAP を使ってドメインと IP の登録情報を調べます。"
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Bcrypt 生成・検証",
+ "subtitle": "bcrypt でパスワードをハッシュ化し、ハッシュを検証 — すべてブラウザ内で完結します",
+ "tabs": {
+ "hash": "ハッシュ",
+ "verify": "検証"
+ },
+ "showPassword": "パスワードを表示",
+ "hidePassword": "パスワードを非表示",
+ "copy": "コピー",
+ "copied": "コピーしました",
+ "hash": {
+ "passwordLabel": "パスワード",
+ "passwordPlaceholder": "ハッシュ化するパスワードを入力…",
+ "costLabel": "コストファクター",
+ "costHint": "コストが高いほど、各ステップの処理量が倍増します。本番環境では 10〜12 が一般的です。",
+ "slowWarning": "コストファクターが 13 以上になると、計算に数秒かかることがあります。",
+ "generate": "ハッシュを生成",
+ "generating": "ハッシュ化中…",
+ "resultLabel": "Bcrypt ハッシュ",
+ "timeTaken": "{ms} ミリ秒",
+ "anatomy": {
+ "title": "ハッシュの構造",
+ "algorithm": "アルゴリズム",
+ "cost": "コスト",
+ "salt": "ソルト(22 文字)",
+ "hash": "ハッシュ(31 文字)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "パスワード",
+ "passwordPlaceholder": "確認するパスワードを入力…",
+ "hashLabel": "Bcrypt ハッシュ",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "パスワードと bcrypt ハッシュを入力して比較します。",
+ "invalidHash": "これは有効な bcrypt ハッシュではないようです。",
+ "checking": "比較中…",
+ "match": "一致 — このパスワードはこのハッシュを生成します。",
+ "noMatch": "不一致 — このパスワードはこのハッシュを生成しません。"
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Chmod 計算機",
+ "subtitle": "Unix ファイルのパーミッションをチェックボックス、8 進数、シンボリック表記の間で変換します",
+ "grid": {
+ "title": "パーミッション",
+ "read": "読み取り",
+ "write": "書き込み",
+ "execute": "実行",
+ "owner": "所有者",
+ "group": "グループ",
+ "others": "その他"
+ },
+ "special": {
+ "title": "特殊ビット",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "8 進数",
+ "placeholder": "755 または 4755",
+ "invalid": "3 桁または 4 桁の 8 進数(0-7)を入力してください。"
+ },
+ "symbolic": {
+ "label": "シンボリック",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "rwxr-xr-x のように 9 文字を入力してください(s/S、t/T も可)。"
+ },
+ "output": {
+ "title": "コマンド",
+ "copy": "コピー",
+ "copied": "コピーしました"
+ },
+ "explain": {
+ "line": "{cls}は{perms}ことができます。",
+ "none": "{cls}にはパーミッションがありません。",
+ "setuid": "Setuid: ファイルは所有者の権限で実行されます。",
+ "setgid": "Setgid: ファイルはグループの権限で実行されます(ディレクトリ内の新規ファイルはそのグループを継承します)。",
+ "sticky": "Sticky: このディレクトリ内では、ファイルの所有者のみが削除や名前変更を行えます。"
+ },
+ "presets": {
+ "title": "よく使うプリセット",
+ "octal": "8 進数",
+ "permissions": "パーミッション",
+ "description": "一般的な用途",
+ "p644": "通常のファイル: 所有者が書き込み、全員が読み取り。",
+ "p755": "実行ファイルとディレクトリ: 全員が読み取りと移動が可能。",
+ "p600": "プライベートファイル: 所有者のみが読み書き。",
+ "p700": "プライベートな実行ファイルとディレクトリ: 所有者のみ。",
+ "p664": "グループが書き込み可能なファイル。",
+ "p775": "グループが書き込み可能なディレクトリと実行ファイル。",
+ "p400": "所有者のみ読み取り可(例: SSH 秘密鍵)。"
+ }
+ },
+ "CodeScreenshot": {
+ "title": "コードスクリーンショット",
+ "subtitle": "コードを美しく共有しやすい画像に変換",
+ "languageLabel": "言語",
+ "autoDetect": "自動検出",
+ "backgroundLabel": "背景",
+ "paddingLabel": "パディング",
+ "filenameLabel": "ファイル名",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "ウィンドウのドット",
+ "themeLabel": "ウィンドウのテーマ",
+ "themeDark": "ダーク",
+ "themeLight": "ライト",
+ "scaleLabel": "書き出し倍率",
+ "copyImage": "画像をコピー",
+ "copied": "コピーしました",
+ "exportPng": "PNG を書き出し",
+ "inputLabel": "コード",
+ "detected": "検出: {language}",
+ "clear": "クリア",
+ "placeholder": "ここにコードを貼り付け…",
+ "previewLabel": "プレビュー",
+ "emptyPreview": "コードのプレビューがここに表示されます",
+ "exportFailed": "画像の書き出しに失敗しました",
+ "copyUnsupported": "このブラウザでは画像のコピーがサポートされていません",
+ "copyFailed": "画像のコピーに失敗しました",
+ "backgrounds": {
+ "ocean": "オーシャン",
+ "sunset": "サンセット",
+ "candy": "キャンディ",
+ "forest": "フォレスト",
+ "midnight": "ミッドナイト",
+ "gold": "ゴールド",
+ "aurora": "オーロラ",
+ "slate": "スレート",
+ "transparent": "透明"
+ }
+ },
+ "CssGenerators": {
+ "title": "CSS ジェネレーター",
+ "subtitle": "Box-shadow、border-radius、流動的な clamp() — CSS をコピー。",
+ "preview": "プレビュー",
+ "output": "CSS",
+ "copy": "コピー",
+ "copied": "コピーしました",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "流動的な clamp()"
+ },
+ "boxShadow": {
+ "presets": "プリセット",
+ "layer": "レイヤー",
+ "addLayer": "レイヤーを追加",
+ "removeLayer": "レイヤーを削除",
+ "x": "X",
+ "y": "Y",
+ "blur": "ぼかし",
+ "spread": "広がり",
+ "color": "色",
+ "alpha": "不透明度",
+ "inset": "内側",
+ "presetNames": {
+ "subtleCard": "控えめなカード",
+ "elevated": "浮き上がり",
+ "hardOffset": "ハードオフセット",
+ "neonGlow": "ネオングロー",
+ "inner": "内側",
+ "layeredSmooth": "重ねたスムーズ"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "モード",
+ "modes": {
+ "simple": "角ごと",
+ "blob": "オーガニックブロブ"
+ },
+ "unit": "単位",
+ "linkCorners": "角を連動",
+ "randomize": "ランダム",
+ "horizontal": "水平方向の半径",
+ "vertical": "垂直方向の半径",
+ "corners": {
+ "tl": "左上",
+ "tr": "右上",
+ "br": "右下",
+ "bl": "左下"
+ }
+ },
+ "clamp": {
+ "minFont": "最小フォント (px)",
+ "maxFont": "最大フォント (px)",
+ "minViewport": "最小ビューポート (px)",
+ "maxViewport": "最大ビューポート (px)",
+ "rootFont": "ルートフォント (px)",
+ "viewportWidth": "シミュレートするビューポート",
+ "computedSize": "計算されたサイズ",
+ "slope": "傾き",
+ "intercept": "切片",
+ "previewText": "The quick brown fox",
+ "invalid": "有効で異なるビューポートの範囲を入力してください。"
+ }
+ },
+ "ExifViewer": {
+ "title": "EXIF ビューアー・除去",
+ "subtitle": "写真のメタデータを確認し、ブラウザ内で除去します",
+ "dropTitle": "ここに画像をドロップ",
+ "dropHint": "またはクリックして選択 — 画像がデバイスから送信されることはありません",
+ "browse": "画像を選択",
+ "formats": "JPEG、PNG、WebP、HEIC、TIFF",
+ "parsing": "メタデータを読み取り中…",
+ "parseFailed": "この画像の読み取りに失敗しました",
+ "clear": "画像を削除",
+ "gpsWarning": "GPS 位置情報を含みます",
+ "openMap": "OpenStreetMap で表示",
+ "noMetadata": "この画像に埋め込まれたメタデータは見つかりませんでした。",
+ "facts": {
+ "name": "名前",
+ "type": "種類",
+ "dimensions": "寸法",
+ "size": "サイズ"
+ },
+ "groups": {
+ "camera": "カメラ",
+ "exposure": "露出",
+ "timestamps": "タイムスタンプ",
+ "gps": "GPS 位置情報",
+ "software": "ソフトウェア・その他"
+ },
+ "gpsCard": {
+ "latitude": "緯度",
+ "longitude": "経度",
+ "altitude": "高度"
+ },
+ "raw": {
+ "title": "すべてのメタデータ",
+ "copyJson": "JSON をコピー",
+ "copied": "コピーしました"
+ },
+ "remove": {
+ "title": "メタデータを除去",
+ "jpegDescription": "画像を再圧縮せずに、EXIF、XMP、IPTC、コメントを可逆的に除去します。",
+ "reencodeDescription": "画像を再エンコードして埋め込まれたメタデータを除去します。ピクセルが再圧縮されます。",
+ "keepIcc": "カラープロファイル (ICC) を保持",
+ "stripButton": "メタデータを除去",
+ "reencodeButton": "再エンコードして除去",
+ "unsupported": "このファイル形式ではメタデータの除去はサポートされていません。",
+ "failed": "メタデータの除去に失敗しました",
+ "before": "変更前",
+ "after": "変更後",
+ "download": "クリーンな画像をダウンロード"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Favicon ジェネレーター",
+ "subtitle": "1 枚の画像から favicon とアプリアイコンを作成",
+ "dropTitle": "ここに画像をドロップ",
+ "dropHint": "PNG、JPEG、SVG — クリックして選択",
+ "browse": "画像を選択",
+ "clear": "画像を削除",
+ "backgroundLabel": "背景",
+ "transparent": "透明",
+ "solidColor": "単色",
+ "cornerLabel": "角の形状",
+ "cornerNone": "四角",
+ "cornerRounded": "角丸",
+ "cornerCircle": "円形",
+ "maskPaddingLabel": "マスカブルの余白",
+ "maskable": "maskable",
+ "previewLabel": "プレビュー",
+ "copy": "コピー",
+ "headSnippet": "HTML head スニペット",
+ "downloadIco": "favicon.ico をダウンロード",
+ "downloadZip": "すべてダウンロード (.zip)"
+ },
+ "JsonDiff": {
+ "title": "JSON 差分",
+ "subtitle": "2 つの JSON ドキュメントを意味的・構造的に比較 — キーの順序や書式は無視されます",
+ "options": {
+ "ignoreArrayOrder": "配列の順序を無視"
+ },
+ "loadBothSamples": "サンプルを読み込む",
+ "sample": "サンプル",
+ "format": "整形",
+ "clear": "クリア",
+ "formatError": "整形できません: 入力が有効な JSON ではありません",
+ "placeholder": "ここに JSON を貼り付けまたは入力…",
+ "panels": {
+ "left": "元 (左)",
+ "right": "変更後 (右)",
+ "results": "差分"
+ },
+ "summary": {
+ "added": "{count} 件追加",
+ "removed": "{count} 件削除",
+ "changed": "{count} 件変更"
+ },
+ "types": {
+ "added": "追加",
+ "removed": "削除",
+ "changed": "変更"
+ },
+ "copyChanges": "JSON としてコピー",
+ "copied": "コピーしました",
+ "empty": "両方のパネルに JSON を貼り付けて差分を確認します。",
+ "identical": "2 つのドキュメントは意味的に同一です。"
+ },
+ "JsonpathPlayground": {
+ "title": "JSONPath プレイグラウンド",
+ "subtitle": "JSONPath または JMESPath で JSON ドキュメントをクエリし、結果をリアルタイムで確認します",
+ "engineLabel": "エンジン",
+ "queryLabel": "クエリ",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "チートシート",
+ "cheats": {
+ "jpAllTitles": "すべての書籍タイトル",
+ "jpAllAuthors": "ドキュメント内のすべての著者",
+ "jpFirstBook": "最初の書籍",
+ "jpLastBook": "最後の書籍",
+ "jpSlice": "最初の 2 冊 (スライス)",
+ "jpFilterPrice": "40 未満の書籍",
+ "jpFilterInStock": "在庫のある書籍のタイトル",
+ "jpRecursive": "すべての値を再帰的に取得",
+ "jmAllTitles": "すべての書籍タイトル",
+ "jmFirstAuthor": "最初の書籍の著者",
+ "jmLastBook": "最後の書籍",
+ "jmSlice": "最初の 2 冊 (スライス)",
+ "jmFilterPrice": "40 未満の書籍のタイトル",
+ "jmCount": "在庫のある書籍を集計",
+ "jmProjection": "各書籍を新しいオブジェクトに再構成",
+ "jmSort": "価格順に並べたタイトル"
+ },
+ "panels": {
+ "input": "JSON 入力",
+ "results": "結果"
+ },
+ "sample": "サンプル",
+ "format": "整形",
+ "clear": "クリア",
+ "formatError": "整形できません: 入力が有効な JSON ではありません",
+ "placeholder": "ここに JSON を貼り付けまたは入力…",
+ "outputPlaceholder": "一致した結果がここに表示されます…",
+ "matchCount": "{count, plural, one {# 件の一致} other {# 件の一致}}",
+ "copy": "コピー",
+ "copied": "コピーしました"
+ },
+ "KeycodeInspector": {
+ "title": "キーコードインスペクター",
+ "subtitle": "任意のキーを押して JavaScript のキーボードイベントを確認します",
+ "capture": {
+ "prompt": "任意のキーを押してください…",
+ "ready": "待機中 — キーボードの任意のキーを押してください",
+ "clickToFocus": "ここをクリックしてフォーカスし、任意のキーを押してください"
+ },
+ "hero": {
+ "spaceKey": "(スペース)",
+ "keyLabel": "event.key",
+ "repeat": "リピート"
+ },
+ "copyJson": "JSON をコピー",
+ "copiedJson": "イベント JSON をクリップボードにコピーしました",
+ "deprecated": "非推奨",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "標準",
+ "left": "左",
+ "right": "右",
+ "numpad": "テンキー",
+ "unknown": "不明"
+ },
+ "history": {
+ "title": "履歴",
+ "clear": "履歴をクリア",
+ "empty": "キー入力がここに表示されます。"
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Markdown テーブルジェネレーター",
+ "subtitle": "グリッドを編集し、スプレッドシートから貼り付け、きれいに整列した Markdown または HTML テーブルをコピーします",
+ "addRow": "行を追加",
+ "addColumn": "列を追加",
+ "importLabel": "インポート",
+ "importPlaceholder": "TSV (Excel / Google スプレッドシート) または CSV をここに貼り付け…",
+ "importButton": "グリッドにインポート",
+ "pasteClipboard": "クリップボードから貼り付け",
+ "importEmpty": "インポートするものがありません — まず行を貼り付けてください",
+ "clipboardError": "クリップボードから読み取れませんでした",
+ "clear": "クリア",
+ "panels": {
+ "editor": "テーブルエディター",
+ "output": "出力"
+ },
+ "align": {
+ "left": "左揃え",
+ "center": "中央揃え",
+ "right": "右揃え"
+ },
+ "headerPlaceholder": "ヘッダー",
+ "cellPlaceholder": "セル",
+ "removeRow": "行を削除",
+ "removeColumn": "列を削除",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "コピー",
+ "copied": "コピーしました"
+ },
+ "TimezoneConverter": {
+ "title": "タイムゾーン変換",
+ "subtitle": "会議の計画のために、ある瞬間を世界中の時刻に変換します。",
+ "dateTimeLabel": "日付と時刻",
+ "now": "現在",
+ "sourceZoneLabel": "変換元のタイムゾーン",
+ "selectZone": "タイムゾーンを選択",
+ "searchZone": "タイムゾーンを検索...",
+ "noZone": "タイムゾーンが見つかりません。",
+ "scrubLabel": "時刻をずらす",
+ "reset": "リセット",
+ "targetsLabel": "変換先のタイムゾーン",
+ "addZone": "タイムゾーンを追加",
+ "remove": "削除",
+ "localBadge": "ローカル",
+ "invalidDate": "有効な日付と時刻を入力してください。",
+ "bands": {
+ "work": "勤務時間 (09:00–18:00)",
+ "edge": "境界の時間帯 (早朝 / 夜)",
+ "night": "夜間"
+ }
+ },
+ "TokenCounter": {
+ "title": "LLM トークンカウンター",
+ "subtitle": "トークンを数え、チャンクを可視化し、API コストを見積もります。",
+ "inputLabel": "テキスト",
+ "sample": "サンプル",
+ "clear": "クリア",
+ "placeholder": "トークンを数えるテキスト、コード、JSON を貼り付け...",
+ "encoding": "エンコーディング",
+ "loading": "トークナイザーを読み込み中...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / o シリーズ)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "トークン",
+ "characters": "文字",
+ "words": "単語",
+ "ratio": "トークン/単語",
+ "ratioSub": "概算"
+ },
+ "claudeRow": "Claude (概算)",
+ "estimate": "見積もり",
+ "viz": {
+ "title": "トークンの可視化",
+ "toggle": "表示",
+ "hint": "切り替えて各トークンをチャンクとして色付けします。",
+ "empty": "テキストを入力してトークンを可視化します。",
+ "truncated": "{total} 個中、最初の {limit} 個のトークンを表示しています。"
+ },
+ "cost": {
+ "title": "コスト計算機",
+ "disclaimer": "編集可能なデフォルト値 — 最新の料金を確認してください",
+ "model": "モデル",
+ "expectedOutput": "想定される出力トークン",
+ "customInput": "入力 $/MTok",
+ "customOutput": "出力 $/MTok",
+ "inputCost": "入力コスト",
+ "outputCost": "出力コスト",
+ "total": "合計",
+ "per1000": "1000 回の呼び出しあたり"
+ }
+ },
+ "WebhookTester": {
+ "title": "Webhook テスター",
+ "subtitle": "一時的な URL を作成し、受信した webhook リクエストをライブで確認します。",
+ "intro": {
+ "title": "webhook URL を作成してリクエストのキャプチャを開始",
+ "hint": "推測できない URL が発行され、受信したすべてのリクエストを記録します。24 時間後に期限切れになります。"
+ },
+ "createBtn": "webhook URL を作成",
+ "creating": "作成中…",
+ "urlLabel": "あなたの webhook URL",
+ "expiresIn": "{countdown} 後に期限切れ",
+ "copyUrlBtn": "URL をコピー",
+ "copied": "コピーしました",
+ "clearBtn": "クリア",
+ "newBinBtn": "新しい URL",
+ "requestsTitle": "リクエスト",
+ "selectPrompt": "リクエストを選択して詳細を確認します。",
+ "expiredNotice": "この webhook URL は期限切れです。テストを続けるには新しく作成してください。",
+ "base64Badge": "base64",
+ "truncatedBadge": "切り詰め",
+ "empty": {
+ "title": "リクエストを待機中…",
+ "hint": "URL にリクエストを送信すると、すぐにここに表示されます。試してみてください:"
+ },
+ "detail": {
+ "receivedAt": "受信日時",
+ "source": "送信元 IP",
+ "contentType": "Content-Type",
+ "query": "クエリパラメーター",
+ "headers": "ヘッダー",
+ "body": "ボディ",
+ "copyBody": "ボディをコピー",
+ "emptyBody": "ボディなし"
+ },
+ "errors": {
+ "createFailed": "webhook URL の作成に失敗しました。もう一度お試しください。",
+ "clearFailed": "リクエストのクリアに失敗しました。もう一度お試しください。"
+ }
+ },
+ "WebsocketTester": {
+ "title": "WebSocket テスター",
+ "subtitle": "WebSocket エンドポイントに接続し、メッセージの送受信と確認をライブで行います",
+ "urlLabel": "エンドポイント URL",
+ "protocolsLabel": "サブプロトコル",
+ "protocolsPlaceholder": "任意、カンマ区切り",
+ "connect": "接続",
+ "disconnect": "切断",
+ "status": {
+ "disconnected": "切断済み",
+ "connecting": "接続中",
+ "connected": "接続済み",
+ "error": "エラー"
+ },
+ "messages": {
+ "connecting": "{url} に接続中…",
+ "connected": "接続が確立されました",
+ "connectedWithProtocol": "接続が確立されました (プロトコル: {protocol})",
+ "invalidUrl": "無効な URL — ws:// または wss:// で始まる必要があります",
+ "connectFailed": "接続を開けませんでした",
+ "socketError": "接続エラーが発生しました",
+ "notConnected": "未接続 — まず接続してください",
+ "binaryMessage": "バイナリメッセージを受信しました"
+ },
+ "log": {
+ "title": "メッセージログ",
+ "empty": "メッセージはまだありません。接続して送信を開始してください。",
+ "copyAll": "すべてコピー",
+ "download": ".txt でダウンロード",
+ "clear": "ログをクリア",
+ "prettyPrint": "JSON を整形",
+ "showRaw": "生テキストを表示"
+ },
+ "composer": {
+ "placeholder": "送信するメッセージを入力…",
+ "send": "送信",
+ "formatJson": "JSON として整形",
+ "invalidJson": "メッセージが有効な JSON ではありません",
+ "hint": "Cmd/Ctrl+Enter で送信"
+ },
+ "ping": {
+ "label": "自動 ping",
+ "seconds": "秒",
+ "intervalTitle": "Ping の間隔 (秒)",
+ "payloadPlaceholder": "ping ペイロード"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Whois 検索",
+ "subtitle": "RDAP を使って任意のドメインや IP アドレスの登録情報を調べます",
+ "placeholder": "example.com または 8.8.8.8",
+ "lookupBtn": "検索",
+ "looking": "検索中…",
+ "refreshBtn": "更新",
+ "tryLabel": "お試し:",
+ "copy": "コピー",
+ "copied": "コピーしました",
+ "notAvailable": "利用できません",
+ "summary": {
+ "registrar": "レジストラ",
+ "network": "ネットワーク"
+ },
+ "dates": {
+ "created": "作成日",
+ "updated": "更新日",
+ "expires": "有効期限"
+ },
+ "sections": {
+ "status": "ステータス",
+ "nameservers": "ネームサーバー",
+ "contacts": "連絡先",
+ "rawRdap": "生の RDAP"
+ },
+ "contacts": {
+ "roles": "役割",
+ "name": "名前",
+ "organization": "組織"
+ },
+ "errors": {
+ "invalidTarget": "有効なドメイン名または IP アドレスを入力してください",
+ "lookupFailed": "検索に失敗しました (ステータス {status})"
+ }
}
}
diff --git a/apps/web/messages/ko.json b/apps/web/messages/ko.json
index 2ed3698c..537728d6 100644
--- a/apps/web/messages/ko.json
+++ b/apps/web/messages/ko.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Chmod 계산기",
+ "codeScreenshot": "코드 스크린샷",
+ "cssGenerators": "CSS 생성기",
+ "exifViewer": "EXIF 뷰어",
+ "faviconGenerator": "Favicon 생성기",
+ "jsonDiff": "JSON 비교",
+ "jsonpathPlayground": "JSONPath 플레이그라운드",
+ "keycodeInspector": "키코드 검사기",
+ "markdownTableGenerator": "Markdown 표",
+ "timezoneConverter": "시간대 변환기",
+ "tokenCounter": "토큰 카운터",
+ "webhookTester": "Webhook 테스터",
+ "websocketTester": "WebSocket 테스터",
+ "whoisLookup": "Whois 조회"
},
"Help": {
"title": "도움말 및 문서",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Bcrypt 생성 및 검증",
+ "description": "bcrypt로 비밀번호를 해시하고 기존 해시를 검증합니다."
+ },
+ "chmodCalculator": {
+ "title": "Chmod 계산기",
+ "description": "Unix 권한을 8진수, 기호 표기, 체크박스 간에 변환합니다."
+ },
+ "codeScreenshot": {
+ "title": "코드 스크린샷",
+ "description": "소스 코드를 아름답고 공유하기 좋은 이미지로 변환합니다."
+ },
+ "cssGenerators": {
+ "title": "CSS 생성기",
+ "description": "Box-shadow, border-radius, 유동적 clamp() 생성기입니다."
+ },
+ "exifViewer": {
+ "title": "EXIF 뷰어 및 제거",
+ "description": "사진의 EXIF 메타데이터를 로컬에서 확인하고 제거합니다."
+ },
+ "faviconGenerator": {
+ "title": "Favicon 생성기",
+ "description": "favicon, 앱 아이콘, 매니페스트 스니펫을 생성합니다."
+ },
+ "jsonDiff": {
+ "title": "JSON 비교",
+ "description": "두 JSON 문서를 의미론적, 구조 인식 방식으로 비교합니다."
+ },
+ "jsonpathPlayground": {
+ "title": "JSONPath 플레이그라운드",
+ "description": "JSONPath 또는 JMESPath로 JSON을 쿼리하고 결과를 실시간으로 확인합니다."
+ },
+ "keycodeInspector": {
+ "title": "키코드 검사기",
+ "description": "키를 눌러 JavaScript 이벤트 코드와 수정 키를 확인합니다."
+ },
+ "markdownTableGenerator": {
+ "title": "Markdown 표 생성기",
+ "description": "스프레드시트 같은 그리드 편집기에서 Markdown 및 HTML 표를 만듭니다."
+ },
+ "timezoneConverter": {
+ "title": "시간대 변환기",
+ "description": "하나의 시각을 여러 시간대로 변환하고 날짜 변동과 근무 시간을 표시합니다."
+ },
+ "tokenCounter": {
+ "title": "LLM 토큰 카운터",
+ "description": "LLM 토큰을 세고 청크를 시각화하며 API 비용을 추정합니다."
+ },
+ "webhookTester": {
+ "title": "Webhook 테스터",
+ "description": "수신되는 webhook 요청을 실시간으로 캡처하고 검사합니다."
+ },
+ "websocketTester": {
+ "title": "WebSocket 테스터",
+ "description": "WebSocket 엔드포인트에 연결하여 메시지를 실시간으로 주고받습니다."
+ },
+ "whoisLookup": {
+ "title": "Whois 조회",
+ "description": "RDAP를 통해 도메인과 IP 등록 정보를 조회합니다."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Bcrypt 생성 및 검증",
+ "subtitle": "bcrypt로 비밀번호를 해시하고 해시를 검증 — 모든 작업이 브라우저 안에서 이루어집니다",
+ "tabs": {
+ "hash": "해시",
+ "verify": "검증"
+ },
+ "showPassword": "비밀번호 표시",
+ "hidePassword": "비밀번호 숨기기",
+ "copy": "복사",
+ "copied": "복사됨",
+ "hash": {
+ "passwordLabel": "비밀번호",
+ "passwordPlaceholder": "해시할 비밀번호를 입력하세요…",
+ "costLabel": "비용 계수",
+ "costHint": "비용이 높을수록 각 단계의 작업량이 두 배가 됩니다. 프로덕션에서는 10~12가 일반적입니다.",
+ "slowWarning": "비용 계수가 13 이상이면 계산에 몇 초가 걸릴 수 있습니다.",
+ "generate": "해시 생성",
+ "generating": "해시 중…",
+ "resultLabel": "Bcrypt 해시",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "해시 구조",
+ "algorithm": "알고리즘",
+ "cost": "비용",
+ "salt": "솔트 (22자)",
+ "hash": "해시 (31자)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "비밀번호",
+ "passwordPlaceholder": "확인할 비밀번호를 입력하세요…",
+ "hashLabel": "Bcrypt 해시",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "비밀번호와 bcrypt 해시를 입력하여 비교합니다.",
+ "invalidHash": "유효한 bcrypt 해시가 아닌 것 같습니다.",
+ "checking": "비교 중…",
+ "match": "일치 — 이 비밀번호가 이 해시를 생성합니다.",
+ "noMatch": "불일치 — 이 비밀번호는 이 해시를 생성하지 않습니다."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Chmod 계산기",
+ "subtitle": "Unix 파일 권한을 체크박스, 8진수, 기호 표기 간에 변환합니다",
+ "grid": {
+ "title": "권한",
+ "read": "읽기",
+ "write": "쓰기",
+ "execute": "실행",
+ "owner": "소유자",
+ "group": "그룹",
+ "others": "기타"
+ },
+ "special": {
+ "title": "특수 비트",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "8진수",
+ "placeholder": "755 또는 4755",
+ "invalid": "3자리 또는 4자리 8진수 (0-7)를 입력하세요."
+ },
+ "symbolic": {
+ "label": "기호 표기",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "rwxr-xr-x처럼 9자를 입력하세요 (s/S, t/T 허용)."
+ },
+ "output": {
+ "title": "명령어",
+ "copy": "복사",
+ "copied": "복사됨"
+ },
+ "explain": {
+ "line": "{cls}은(는) {perms} 수 있습니다.",
+ "none": "{cls}에는 권한이 없습니다.",
+ "setuid": "Setuid: 파일이 소유자의 권한으로 실행됩니다.",
+ "setgid": "Setgid: 파일이 그룹의 권한으로 실행됩니다 (디렉터리의 새 파일은 해당 그룹을 상속합니다).",
+ "sticky": "Sticky: 이 디렉터리 안에서는 파일 소유자만 파일을 삭제하거나 이름을 바꿀 수 있습니다."
+ },
+ "presets": {
+ "title": "일반 프리셋",
+ "octal": "8진수",
+ "permissions": "권한",
+ "description": "일반적인 용도",
+ "p644": "일반 파일: 소유자 쓰기, 모두 읽기.",
+ "p755": "실행 파일 및 디렉터리: 모두 읽고 진입 가능.",
+ "p600": "비공개 파일: 소유자만 읽고 쓰기.",
+ "p700": "비공개 실행 파일 및 디렉터리: 소유자 전용.",
+ "p664": "그룹 쓰기 가능 파일.",
+ "p775": "그룹 쓰기 가능 디렉터리 및 실행 파일.",
+ "p400": "소유자 읽기 전용 (예: SSH 개인 키)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "코드 스크린샷",
+ "subtitle": "코드를 아름답고 공유하기 좋은 이미지로 변환",
+ "languageLabel": "언어",
+ "autoDetect": "자동 감지",
+ "backgroundLabel": "배경",
+ "paddingLabel": "여백",
+ "filenameLabel": "파일 이름",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "창 점",
+ "themeLabel": "창 테마",
+ "themeDark": "다크",
+ "themeLight": "라이트",
+ "scaleLabel": "내보내기 배율",
+ "copyImage": "이미지 복사",
+ "copied": "복사됨",
+ "exportPng": "PNG 내보내기",
+ "inputLabel": "코드",
+ "detected": "감지됨: {language}",
+ "clear": "지우기",
+ "placeholder": "여기에 코드를 붙여넣으세요…",
+ "previewLabel": "미리보기",
+ "emptyPreview": "코드 미리보기가 여기에 표시됩니다",
+ "exportFailed": "이미지 내보내기에 실패했습니다",
+ "copyUnsupported": "이 브라우저에서는 이미지 복사가 지원되지 않습니다",
+ "copyFailed": "이미지 복사에 실패했습니다",
+ "backgrounds": {
+ "ocean": "오션",
+ "sunset": "선셋",
+ "candy": "캔디",
+ "forest": "포레스트",
+ "midnight": "미드나이트",
+ "gold": "골드",
+ "aurora": "오로라",
+ "slate": "슬레이트",
+ "transparent": "투명"
+ }
+ },
+ "CssGenerators": {
+ "title": "CSS 생성기",
+ "subtitle": "Box-shadow, border-radius, 유동적 clamp() — CSS를 복사하세요.",
+ "preview": "미리보기",
+ "output": "CSS",
+ "copy": "복사",
+ "copied": "복사됨",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "유동적 clamp()"
+ },
+ "boxShadow": {
+ "presets": "프리셋",
+ "layer": "레이어",
+ "addLayer": "레이어 추가",
+ "removeLayer": "레이어 제거",
+ "x": "X",
+ "y": "Y",
+ "blur": "흐림",
+ "spread": "확산",
+ "color": "색상",
+ "alpha": "투명도",
+ "inset": "안쪽",
+ "presetNames": {
+ "subtleCard": "은은한 카드",
+ "elevated": "떠 있는",
+ "hardOffset": "하드 오프셋",
+ "neonGlow": "네온 글로우",
+ "inner": "안쪽",
+ "layeredSmooth": "겹친 부드러움"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "모드",
+ "modes": {
+ "simple": "모서리별",
+ "blob": "유기적 블롭"
+ },
+ "unit": "단위",
+ "linkCorners": "모서리 연결",
+ "randomize": "무작위",
+ "horizontal": "수평 반경",
+ "vertical": "수직 반경",
+ "corners": {
+ "tl": "왼쪽 위",
+ "tr": "오른쪽 위",
+ "br": "오른쪽 아래",
+ "bl": "왼쪽 아래"
+ }
+ },
+ "clamp": {
+ "minFont": "최소 글꼴 (px)",
+ "maxFont": "최대 글꼴 (px)",
+ "minViewport": "최소 뷰포트 (px)",
+ "maxViewport": "최대 뷰포트 (px)",
+ "rootFont": "루트 글꼴 (px)",
+ "viewportWidth": "시뮬레이션 뷰포트",
+ "computedSize": "계산된 크기",
+ "slope": "기울기",
+ "intercept": "절편",
+ "previewText": "The quick brown fox",
+ "invalid": "유효하고 서로 다른 뷰포트 범위를 입력하세요."
+ }
+ },
+ "ExifViewer": {
+ "title": "EXIF 뷰어 및 제거",
+ "subtitle": "사진 메타데이터를 확인하고 브라우저에서 제거합니다",
+ "dropTitle": "여기에 이미지를 놓으세요",
+ "dropHint": "또는 클릭하여 찾아보기 — 이미지는 기기를 벗어나지 않습니다",
+ "browse": "이미지 선택",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "메타데이터 읽는 중…",
+ "parseFailed": "이 이미지를 읽지 못했습니다",
+ "clear": "이미지 제거",
+ "gpsWarning": "GPS 위치 정보 포함",
+ "openMap": "OpenStreetMap에서 보기",
+ "noMetadata": "이 이미지에서 삽입된 메타데이터를 찾지 못했습니다.",
+ "facts": {
+ "name": "이름",
+ "type": "형식",
+ "dimensions": "크기",
+ "size": "용량"
+ },
+ "groups": {
+ "camera": "카메라",
+ "exposure": "노출",
+ "timestamps": "타임스탬프",
+ "gps": "GPS 위치",
+ "software": "소프트웨어 및 기타"
+ },
+ "gpsCard": {
+ "latitude": "위도",
+ "longitude": "경도",
+ "altitude": "고도"
+ },
+ "raw": {
+ "title": "모든 메타데이터",
+ "copyJson": "JSON 복사",
+ "copied": "복사됨"
+ },
+ "remove": {
+ "title": "메타데이터 제거",
+ "jpegDescription": "이미지를 재압축하지 않고 EXIF, XMP, IPTC, 주석을 무손실로 제거합니다.",
+ "reencodeDescription": "이미지를 다시 인코딩하여 삽입된 메타데이터를 제거합니다. 픽셀이 재압축됩니다.",
+ "keepIcc": "색상 프로필 (ICC) 유지",
+ "stripButton": "메타데이터 제거",
+ "reencodeButton": "다시 인코딩 후 제거",
+ "unsupported": "이 파일 형식에서는 메타데이터 제거가 지원되지 않습니다.",
+ "failed": "메타데이터 제거에 실패했습니다",
+ "before": "이전",
+ "after": "이후",
+ "download": "정리된 이미지 다운로드"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Favicon 생성기",
+ "subtitle": "하나의 이미지로 favicon과 앱 아이콘을 만듭니다",
+ "dropTitle": "여기에 이미지를 놓으세요",
+ "dropHint": "PNG, JPEG 또는 SVG — 클릭하여 찾아보기",
+ "browse": "이미지 선택",
+ "clear": "이미지 제거",
+ "backgroundLabel": "배경",
+ "transparent": "투명",
+ "solidColor": "단색",
+ "cornerLabel": "모서리 모양",
+ "cornerNone": "사각형",
+ "cornerRounded": "둥근 모서리",
+ "cornerCircle": "원형",
+ "maskPaddingLabel": "마스커블 여백",
+ "maskable": "maskable",
+ "previewLabel": "미리보기",
+ "copy": "복사",
+ "headSnippet": "HTML head 스니펫",
+ "downloadIco": "favicon.ico 다운로드",
+ "downloadZip": "모두 다운로드 (.zip)"
+ },
+ "JsonDiff": {
+ "title": "JSON 비교",
+ "subtitle": "두 JSON 문서를 의미론적, 구조 인식 방식으로 비교 — 키 순서와 서식은 무시됩니다",
+ "options": {
+ "ignoreArrayOrder": "배열 순서 무시"
+ },
+ "loadBothSamples": "샘플 불러오기",
+ "sample": "샘플",
+ "format": "포맷",
+ "clear": "지우기",
+ "formatError": "포맷할 수 없음: 입력이 유효한 JSON이 아닙니다",
+ "placeholder": "여기에 JSON을 붙여넣거나 입력하세요…",
+ "panels": {
+ "left": "원본 (왼쪽)",
+ "right": "수정됨 (오른쪽)",
+ "results": "차이"
+ },
+ "summary": {
+ "added": "{count}개 추가",
+ "removed": "{count}개 제거",
+ "changed": "{count}개 변경"
+ },
+ "types": {
+ "added": "추가됨",
+ "removed": "제거됨",
+ "changed": "변경됨"
+ },
+ "copyChanges": "JSON으로 복사",
+ "copied": "복사됨",
+ "empty": "두 패널에 JSON을 붙여넣어 차이를 확인하세요.",
+ "identical": "두 문서는 의미론적으로 동일합니다."
+ },
+ "JsonpathPlayground": {
+ "title": "JSONPath 플레이그라운드",
+ "subtitle": "JSONPath 또는 JMESPath로 JSON 문서를 쿼리하고 결과를 실시간으로 확인합니다",
+ "engineLabel": "엔진",
+ "queryLabel": "쿼리",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "치트시트",
+ "cheats": {
+ "jpAllTitles": "모든 책 제목",
+ "jpAllAuthors": "문서 어디에나 있는 모든 저자",
+ "jpFirstBook": "첫 번째 책",
+ "jpLastBook": "마지막 책",
+ "jpSlice": "처음 두 권 (슬라이스)",
+ "jpFilterPrice": "40 미만인 책",
+ "jpFilterInStock": "재고 있는 책의 제목",
+ "jpRecursive": "모든 값 재귀적으로",
+ "jmAllTitles": "모든 책 제목",
+ "jmFirstAuthor": "첫 번째 책의 저자",
+ "jmLastBook": "마지막 책",
+ "jmSlice": "처음 두 권 (슬라이스)",
+ "jmFilterPrice": "40 미만인 책의 제목",
+ "jmCount": "재고 있는 책 세기",
+ "jmProjection": "각 책을 새 객체로 재구성",
+ "jmSort": "가격순으로 정렬한 제목"
+ },
+ "panels": {
+ "input": "JSON 입력",
+ "results": "결과"
+ },
+ "sample": "샘플",
+ "format": "포맷",
+ "clear": "지우기",
+ "formatError": "포맷할 수 없음: 입력이 유효한 JSON이 아닙니다",
+ "placeholder": "여기에 JSON을 붙여넣거나 입력하세요…",
+ "outputPlaceholder": "일치 항목이 여기에 표시됩니다…",
+ "matchCount": "{count, plural, one {일치 #개} other {일치 #개}}",
+ "copy": "복사",
+ "copied": "복사됨"
+ },
+ "KeycodeInspector": {
+ "title": "키코드 검사기",
+ "subtitle": "아무 키나 눌러 JavaScript 키보드 이벤트를 검사합니다",
+ "capture": {
+ "prompt": "아무 키나 누르세요…",
+ "ready": "수신 중 — 키보드에서 아무 키나 누르세요",
+ "clickToFocus": "여기를 클릭하여 포커스한 다음 아무 키나 누르세요"
+ },
+ "hero": {
+ "spaceKey": "(스페이스)",
+ "keyLabel": "event.key",
+ "repeat": "반복"
+ },
+ "copyJson": "JSON 복사",
+ "copiedJson": "이벤트 JSON을 클립보드에 복사했습니다",
+ "deprecated": "더 이상 사용되지 않음",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "표준",
+ "left": "왼쪽",
+ "right": "오른쪽",
+ "numpad": "숫자 패드",
+ "unknown": "알 수 없음"
+ },
+ "history": {
+ "title": "기록",
+ "clear": "기록 지우기",
+ "empty": "키 입력이 여기에 표시됩니다."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Markdown 표 생성기",
+ "subtitle": "그리드를 편집하고, 스프레드시트에서 붙여넣고, 깔끔하게 정렬된 Markdown 또는 HTML 표를 복사합니다",
+ "addRow": "행 추가",
+ "addColumn": "열 추가",
+ "importLabel": "가져오기",
+ "importPlaceholder": "TSV (Excel / Google Sheets) 또는 CSV를 여기에 붙여넣으세요…",
+ "importButton": "그리드로 가져오기",
+ "pasteClipboard": "클립보드에서 붙여넣기",
+ "importEmpty": "가져올 항목이 없습니다 — 먼저 행을 붙여넣으세요",
+ "clipboardError": "클립보드에서 읽을 수 없습니다",
+ "clear": "지우기",
+ "panels": {
+ "editor": "표 편집기",
+ "output": "출력"
+ },
+ "align": {
+ "left": "왼쪽 정렬",
+ "center": "가운데 정렬",
+ "right": "오른쪽 정렬"
+ },
+ "headerPlaceholder": "헤더",
+ "cellPlaceholder": "셀",
+ "removeRow": "행 제거",
+ "removeColumn": "열 제거",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "복사",
+ "copied": "복사됨"
+ },
+ "TimezoneConverter": {
+ "title": "시간대 변환기",
+ "subtitle": "회의 계획을 위해 하나의 순간을 전 세계 시각으로 변환합니다.",
+ "dateTimeLabel": "날짜 및 시간",
+ "now": "지금",
+ "sourceZoneLabel": "원본 시간대",
+ "selectZone": "시간대 선택",
+ "searchZone": "시간대 검색...",
+ "noZone": "시간대를 찾을 수 없습니다.",
+ "scrubLabel": "시간 이동",
+ "reset": "재설정",
+ "targetsLabel": "대상 시간대",
+ "addZone": "시간대 추가",
+ "remove": "제거",
+ "localBadge": "로컬",
+ "invalidDate": "유효한 날짜와 시간을 입력하세요.",
+ "bands": {
+ "work": "근무 시간 (09:00–18:00)",
+ "edge": "경계 시간 (이른 아침 / 저녁)",
+ "night": "야간"
+ }
+ },
+ "TokenCounter": {
+ "title": "LLM 토큰 카운터",
+ "subtitle": "토큰을 세고, 청크를 시각화하며, API 비용을 추정합니다.",
+ "inputLabel": "텍스트",
+ "sample": "샘플",
+ "clear": "지우기",
+ "placeholder": "토큰을 셀 텍스트, 코드 또는 JSON을 붙여넣으세요...",
+ "encoding": "인코딩",
+ "loading": "토크나이저 불러오는 중...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / o 시리즈)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "토큰",
+ "characters": "문자",
+ "words": "단어",
+ "ratio": "토큰/단어",
+ "ratioSub": "대략"
+ },
+ "claudeRow": "Claude (근사치)",
+ "estimate": "추정",
+ "viz": {
+ "title": "토큰 시각화",
+ "toggle": "표시",
+ "hint": "전환하여 각 토큰을 청크로 색상 표시합니다.",
+ "empty": "텍스트를 입력하여 토큰을 시각화하세요.",
+ "truncated": "전체 {total}개 중 처음 {limit}개 토큰을 표시하고 있습니다."
+ },
+ "cost": {
+ "title": "비용 계산기",
+ "disclaimer": "편집 가능한 기본값 — 현재 가격을 확인하세요",
+ "model": "모델",
+ "expectedOutput": "예상 출력 토큰",
+ "customInput": "입력 $/MTok",
+ "customOutput": "출력 $/MTok",
+ "inputCost": "입력 비용",
+ "outputCost": "출력 비용",
+ "total": "합계",
+ "per1000": "1000회 호출당"
+ }
+ },
+ "WebhookTester": {
+ "title": "Webhook 테스터",
+ "subtitle": "임시 URL을 만들고 수신되는 webhook 요청을 실시간으로 검사합니다.",
+ "intro": {
+ "title": "webhook URL을 만들어 요청 캡처를 시작하세요",
+ "hint": "추측할 수 없는 URL이 발급되어 수신되는 모든 요청을 기록합니다. 24시간 후에 만료됩니다."
+ },
+ "createBtn": "webhook URL 만들기",
+ "creating": "만드는 중…",
+ "urlLabel": "내 webhook URL",
+ "expiresIn": "{countdown} 후 만료",
+ "copyUrlBtn": "URL 복사",
+ "copied": "복사됨",
+ "clearBtn": "지우기",
+ "newBinBtn": "새 URL",
+ "requestsTitle": "요청",
+ "selectPrompt": "요청을 선택하여 세부 정보를 확인하세요.",
+ "expiredNotice": "이 webhook URL은 만료되었습니다. 계속 테스트하려면 새로 만드세요.",
+ "base64Badge": "base64",
+ "truncatedBadge": "잘림",
+ "empty": {
+ "title": "요청을 기다리는 중…",
+ "hint": "URL로 요청을 보내면 즉시 여기에 표시됩니다. 이렇게 해보세요:"
+ },
+ "detail": {
+ "receivedAt": "수신 시각",
+ "source": "출발지 IP",
+ "contentType": "Content-Type",
+ "query": "쿼리 매개변수",
+ "headers": "헤더",
+ "body": "본문",
+ "copyBody": "본문 복사",
+ "emptyBody": "본문 없음"
+ },
+ "errors": {
+ "createFailed": "webhook URL 생성에 실패했습니다. 다시 시도하세요.",
+ "clearFailed": "요청을 지우지 못했습니다. 다시 시도하세요."
+ }
+ },
+ "WebsocketTester": {
+ "title": "WebSocket 테스터",
+ "subtitle": "WebSocket 엔드포인트에 연결하여 메시지를 실시간으로 전송, 수신, 검사합니다",
+ "urlLabel": "엔드포인트 URL",
+ "protocolsLabel": "하위 프로토콜",
+ "protocolsPlaceholder": "선택 사항, 쉼표로 구분",
+ "connect": "연결",
+ "disconnect": "연결 해제",
+ "status": {
+ "disconnected": "연결 해제됨",
+ "connecting": "연결 중",
+ "connected": "연결됨",
+ "error": "오류"
+ },
+ "messages": {
+ "connecting": "{url}에 연결 중…",
+ "connected": "연결이 설정되었습니다",
+ "connectedWithProtocol": "연결이 설정되었습니다 (프로토콜: {protocol})",
+ "invalidUrl": "잘못된 URL — ws:// 또는 wss://로 시작해야 합니다",
+ "connectFailed": "연결을 열지 못했습니다",
+ "socketError": "연결 오류가 발생했습니다",
+ "notConnected": "연결되지 않음 — 먼저 연결하세요",
+ "binaryMessage": "바이너리 메시지를 수신했습니다"
+ },
+ "log": {
+ "title": "메시지 로그",
+ "empty": "아직 메시지가 없습니다. 연결하고 전송을 시작하세요.",
+ "copyAll": "모두 복사",
+ "download": ".txt로 다운로드",
+ "clear": "로그 지우기",
+ "prettyPrint": "JSON 정렬 표시",
+ "showRaw": "원본 텍스트 표시"
+ },
+ "composer": {
+ "placeholder": "전송할 메시지를 입력하세요…",
+ "send": "전송",
+ "formatJson": "JSON으로 포맷",
+ "invalidJson": "메시지가 유효한 JSON이 아닙니다",
+ "hint": "Cmd/Ctrl+Enter로 전송"
+ },
+ "ping": {
+ "label": "자동 ping",
+ "seconds": "초",
+ "intervalTitle": "Ping 간격 (초)",
+ "payloadPlaceholder": "ping 페이로드"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Whois 조회",
+ "subtitle": "RDAP를 통해 도메인이나 IP 주소의 등록 정보를 조회합니다",
+ "placeholder": "example.com 또는 8.8.8.8",
+ "lookupBtn": "조회",
+ "looking": "조회 중…",
+ "refreshBtn": "새로고침",
+ "tryLabel": "예시:",
+ "copy": "복사",
+ "copied": "복사됨",
+ "notAvailable": "사용할 수 없음",
+ "summary": {
+ "registrar": "등록 대행자",
+ "network": "네트워크"
+ },
+ "dates": {
+ "created": "생성일",
+ "updated": "수정일",
+ "expires": "만료일"
+ },
+ "sections": {
+ "status": "상태",
+ "nameservers": "네임서버",
+ "contacts": "연락처",
+ "rawRdap": "원본 RDAP"
+ },
+ "contacts": {
+ "roles": "역할",
+ "name": "이름",
+ "organization": "조직"
+ },
+ "errors": {
+ "invalidTarget": "유효한 도메인 이름이나 IP 주소를 입력하세요",
+ "lookupFailed": "조회 실패 (상태 {status})"
+ }
}
}
diff --git a/apps/web/messages/ms.json b/apps/web/messages/ms.json
index 509c7f56..837a4b32 100644
--- a/apps/web/messages/ms.json
+++ b/apps/web/messages/ms.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Kalkulator Chmod",
+ "codeScreenshot": "Tangkap Skrin Kod",
+ "cssGenerators": "Penjana CSS",
+ "exifViewer": "Pemapar EXIF",
+ "faviconGenerator": "Penjana Favicon",
+ "jsonDiff": "Beza JSON",
+ "jsonpathPlayground": "Ruang Uji JSONPath",
+ "keycodeInspector": "Pemeriksa Kod Kekunci",
+ "markdownTableGenerator": "Jadual Markdown",
+ "timezoneConverter": "Penukar Zon Waktu",
+ "tokenCounter": "Pengira Token",
+ "webhookTester": "Penguji Webhook",
+ "websocketTester": "Penguji WebSocket",
+ "whoisLookup": "Carian Whois"
},
"Help": {
"title": "Bantuan & dokumentasi",
@@ -532,6 +547,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Penjana & Penyemak Bcrypt",
+ "description": "Cincang kata laluan dengan bcrypt dan sahkan cincangan sedia ada."
+ },
+ "chmodCalculator": {
+ "title": "Kalkulator Chmod",
+ "description": "Tukar keizinan Unix antara oktal, simbolik dan kotak semak."
+ },
+ "codeScreenshot": {
+ "title": "Tangkap Skrin Kod",
+ "description": "Tukar kod sumber menjadi imej cantik yang boleh dikongsi."
+ },
+ "cssGenerators": {
+ "title": "Penjana CSS",
+ "description": "Penjana box-shadow, border-radius dan clamp() bendalir."
+ },
+ "exifViewer": {
+ "title": "Pemapar & Pembuang EXIF",
+ "description": "Periksa dan buang metadata EXIF foto secara setempat."
+ },
+ "faviconGenerator": {
+ "title": "Penjana Favicon",
+ "description": "Jana favicon, ikon aplikasi dan cebisan manifest."
+ },
+ "jsonDiff": {
+ "title": "Beza JSON",
+ "description": "Perbandingan bermakna dan peka struktur bagi dua dokumen JSON."
+ },
+ "jsonpathPlayground": {
+ "title": "Ruang Uji JSONPath",
+ "description": "Kueri JSON dengan JSONPath atau JMESPath dan lihat keputusan secara langsung."
+ },
+ "keycodeInspector": {
+ "title": "Pemeriksa Kod Kekunci",
+ "description": "Tekan satu kekunci untuk lihat kod peristiwa JavaScript dan pengubah suainya."
+ },
+ "markdownTableGenerator": {
+ "title": "Penjana Jadual Markdown",
+ "description": "Bina jadual Markdown dan HTML dalam penyunting grid seperti hamparan."
+ },
+ "timezoneConverter": {
+ "title": "Penukar Zon Waktu",
+ "description": "Tukar satu masa merentas banyak zon dengan petunjuk anjakan hari dan waktu kerja."
+ },
+ "tokenCounter": {
+ "title": "Pengira Token LLM",
+ "description": "Kira token LLM, visualkan cebisan dan anggarkan kos API."
+ },
+ "webhookTester": {
+ "title": "Penguji Webhook",
+ "description": "Tangkap dan periksa permintaan webhook masuk secara masa nyata."
+ },
+ "websocketTester": {
+ "title": "Penguji WebSocket",
+ "description": "Sambung ke titik akhir WebSocket dan bertukar mesej secara masa nyata."
+ },
+ "whoisLookup": {
+ "title": "Carian Whois",
+ "description": "Cari butiran pendaftaran domain dan IP melalui RDAP."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Penjana & Penyemak Bcrypt",
+ "subtitle": "Cincang kata laluan dengan bcrypt dan sahkan cincangan — semuanya kekal dalam pelayar anda",
+ "tabs": {
+ "hash": "Cincang",
+ "verify": "Sahkan"
+ },
+ "showPassword": "Tunjukkan kata laluan",
+ "hidePassword": "Sembunyikan kata laluan",
+ "copy": "Salin",
+ "copied": "Disalin",
+ "hash": {
+ "passwordLabel": "Kata laluan",
+ "passwordPlaceholder": "Masukkan kata laluan untuk dicincang…",
+ "costLabel": "Faktor kos",
+ "costHint": "Kos lebih tinggi menggandakan kerja setiap langkah. 10-12 ialah pilihan biasa untuk pengeluaran.",
+ "slowWarning": "Faktor kos 13 dan ke atas boleh mengambil beberapa saat untuk dikira.",
+ "generate": "Jana cincangan",
+ "generating": "Mencincang…",
+ "resultLabel": "Cincangan Bcrypt",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "Anatomi cincangan",
+ "algorithm": "algoritma",
+ "cost": "kos",
+ "salt": "salt (22 aksara)",
+ "hash": "cincangan (31 aksara)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Kata laluan",
+ "passwordPlaceholder": "Masukkan kata laluan untuk disemak…",
+ "hashLabel": "Cincangan Bcrypt",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Masukkan kata laluan dan cincangan bcrypt untuk membandingkannya.",
+ "invalidHash": "Ini tidak kelihatan seperti cincangan bcrypt yang sah.",
+ "checking": "Membandingkan…",
+ "match": "Padan — kata laluan menghasilkan cincangan ini.",
+ "noMatch": "Tidak padan — kata laluan tidak menghasilkan cincangan ini."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Kalkulator Chmod",
+ "subtitle": "Tukar keizinan fail Unix antara kotak semak, oktal dan tatatanda simbolik",
+ "grid": {
+ "title": "Keizinan",
+ "read": "Baca",
+ "write": "Tulis",
+ "execute": "Laksana",
+ "owner": "Pemilik",
+ "group": "Kumpulan",
+ "others": "Lain-lain"
+ },
+ "special": {
+ "title": "Bit khas",
+ "setuid": "setuid",
+ "setgid": "setgid",
+ "sticky": "sticky"
+ },
+ "octal": {
+ "label": "Oktal",
+ "placeholder": "755 atau 4755",
+ "invalid": "Masukkan 3 atau 4 digit oktal (0-7)."
+ },
+ "symbolic": {
+ "label": "Simbolik",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Masukkan 9 aksara seperti rwxr-xr-x (s/S, t/T dibenarkan)."
+ },
+ "output": {
+ "title": "Arahan",
+ "copy": "Salin",
+ "copied": "Disalin"
+ },
+ "explain": {
+ "line": "{cls} boleh {perms}.",
+ "none": "{cls} tiada sebarang keizinan.",
+ "setuid": "setuid: fail berjalan dengan keistimewaan pemiliknya.",
+ "setgid": "setgid: fail berjalan dengan keistimewaan kumpulannya (fail baharu dalam direktori mewarisi kumpulannya).",
+ "sticky": "sticky: hanya pemilik fail boleh memadam atau menamakan semulanya di dalam direktori ini."
+ },
+ "presets": {
+ "title": "Praset lazim",
+ "octal": "Oktal",
+ "permissions": "Keizinan",
+ "description": "Kegunaan tipikal",
+ "p644": "Fail biasa: pemilik menulis, semua orang membaca.",
+ "p755": "Boleh laku dan direktori: semua orang boleh baca dan masuk.",
+ "p600": "Fail peribadi: hanya pemilik membaca dan menulis.",
+ "p700": "Boleh laku dan direktori peribadi: pemilik sahaja.",
+ "p664": "Fail boleh ditulis oleh kumpulan.",
+ "p775": "Direktori dan boleh laku yang boleh ditulis oleh kumpulan.",
+ "p400": "Baca sahaja untuk pemilik (cth. kunci peribadi SSH)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Tangkap Skrin Kod",
+ "subtitle": "Tukar kod menjadi imej cantik yang boleh dikongsi",
+ "languageLabel": "Bahasa",
+ "autoDetect": "Auto kesan",
+ "backgroundLabel": "Latar belakang",
+ "paddingLabel": "Pelapik",
+ "filenameLabel": "Nama fail",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "Titik tetingkap",
+ "themeLabel": "Tema tetingkap",
+ "themeDark": "Gelap",
+ "themeLight": "Cerah",
+ "scaleLabel": "Skala eksport",
+ "copyImage": "Salin imej",
+ "copied": "Disalin",
+ "exportPng": "Eksport PNG",
+ "inputLabel": "Kod",
+ "detected": "Dikesan: {language}",
+ "clear": "Kosongkan",
+ "placeholder": "Tampal kod anda di sini…",
+ "previewLabel": "Pratonton",
+ "emptyPreview": "Pratonton kod anda akan muncul di sini",
+ "exportFailed": "Gagal mengeksport imej",
+ "copyUnsupported": "Menyalin imej tidak disokong dalam pelayar ini",
+ "copyFailed": "Gagal menyalin imej",
+ "backgrounds": {
+ "ocean": "Lautan",
+ "sunset": "Senja",
+ "candy": "Gula-gula",
+ "forest": "Hutan",
+ "midnight": "Tengah malam",
+ "gold": "Emas",
+ "aurora": "Aurora",
+ "slate": "Batu tulis",
+ "transparent": "Lutsinar"
+ }
+ },
+ "CssGenerators": {
+ "title": "Penjana CSS",
+ "subtitle": "box-shadow, border-radius dan clamp() bendalir — salin CSS.",
+ "preview": "Pratonton",
+ "output": "CSS",
+ "copy": "Salin",
+ "copied": "Disalin",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "clamp() bendalir"
+ },
+ "boxShadow": {
+ "presets": "Praset",
+ "layer": "Lapisan",
+ "addLayer": "Tambah lapisan",
+ "removeLayer": "Buang lapisan",
+ "x": "X",
+ "y": "Y",
+ "blur": "Kabur",
+ "spread": "Sebaran",
+ "color": "Warna",
+ "alpha": "Alfa",
+ "inset": "Terbenam",
+ "presetNames": {
+ "subtleCard": "Kad halus",
+ "elevated": "Tinggi",
+ "hardOffset": "Ofset tegas",
+ "neonGlow": "Sinar neon",
+ "inner": "Dalaman",
+ "layeredSmooth": "Berlapis licin"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Mod",
+ "modes": {
+ "simple": "Setiap sudut",
+ "blob": "Blob organik"
+ },
+ "unit": "Unit",
+ "linkCorners": "Pautkan sudut",
+ "randomize": "Rawakkan",
+ "horizontal": "Jejari mendatar",
+ "vertical": "Jejari menegak",
+ "corners": {
+ "tl": "Kiri atas",
+ "tr": "Kanan atas",
+ "br": "Kanan bawah",
+ "bl": "Kiri bawah"
+ }
+ },
+ "clamp": {
+ "minFont": "Fon min (px)",
+ "maxFont": "Fon maks (px)",
+ "minViewport": "Viewport min (px)",
+ "maxViewport": "Viewport maks (px)",
+ "rootFont": "Fon akar (px)",
+ "viewportWidth": "Viewport simulasi",
+ "computedSize": "Saiz terkira",
+ "slope": "Cerun",
+ "intercept": "Pintasan",
+ "previewText": "Teks contoh pratonton",
+ "invalid": "Masukkan had viewport yang sah dan berbeza."
+ }
+ },
+ "ExifViewer": {
+ "title": "Pemapar & Pembuang EXIF",
+ "subtitle": "Periksa metadata foto dan buang ia dalam pelayar anda",
+ "dropTitle": "Lepaskan imej di sini",
+ "dropHint": "atau klik untuk semak lalu — imej anda tidak pernah meninggalkan peranti anda",
+ "browse": "Pilih imej",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Membaca metadata…",
+ "parseFailed": "Gagal membaca imej ini",
+ "clear": "Buang imej",
+ "gpsWarning": "Mengandungi lokasi GPS",
+ "openMap": "Lihat pada OpenStreetMap",
+ "noMetadata": "Tiada metadata terbenam ditemui dalam imej ini.",
+ "facts": {
+ "name": "Nama",
+ "type": "Jenis",
+ "dimensions": "Dimensi",
+ "size": "Saiz"
+ },
+ "groups": {
+ "camera": "Kamera",
+ "exposure": "Dedahan",
+ "timestamps": "Cap masa",
+ "gps": "Lokasi GPS",
+ "software": "Perisian & Lain-lain"
+ },
+ "gpsCard": {
+ "latitude": "Latitud",
+ "longitude": "Longitud",
+ "altitude": "Altitud"
+ },
+ "raw": {
+ "title": "Semua metadata",
+ "copyJson": "Salin JSON",
+ "copied": "Disalin"
+ },
+ "remove": {
+ "title": "Buang metadata",
+ "jpegDescription": "Buang EXIF, XMP, IPTC dan komen tanpa kehilangan dan tanpa memampatkan semula imej.",
+ "reencodeDescription": "Kod semula imej untuk membuang metadata terbenam. Ini memampatkan semula piksel.",
+ "keepIcc": "Kekalkan profil warna (ICC)",
+ "stripButton": "Buang metadata",
+ "reencodeButton": "Kod semula & buang",
+ "unsupported": "Pembuangan metadata tidak disokong untuk jenis fail ini.",
+ "failed": "Gagal membuang metadata",
+ "before": "Sebelum",
+ "after": "Selepas",
+ "download": "Muat turun imej yang dibersihkan"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Penjana Favicon",
+ "subtitle": "Cipta favicon dan ikon aplikasi daripada satu imej",
+ "dropTitle": "Lepaskan imej di sini",
+ "dropHint": "PNG, JPEG atau SVG — klik untuk semak lalu",
+ "browse": "Pilih imej",
+ "clear": "Buang imej",
+ "backgroundLabel": "Latar belakang",
+ "transparent": "Lutsinar",
+ "solidColor": "Warna pejal",
+ "cornerLabel": "Bentuk sudut",
+ "cornerNone": "Segi empat sama",
+ "cornerRounded": "Bulat",
+ "cornerCircle": "Bulatan",
+ "maskPaddingLabel": "Pelapik boleh topeng",
+ "maskable": "maskable",
+ "previewLabel": "Pratonton",
+ "copy": "Salin",
+ "headSnippet": "Cebisan HTML head",
+ "downloadIco": "Muat turun favicon.ico",
+ "downloadZip": "Muat turun semua (.zip)"
+ },
+ "JsonDiff": {
+ "title": "Beza JSON",
+ "subtitle": "Perbandingan bermakna dan peka struktur bagi dua dokumen JSON — susunan kunci dan pemformatan diabaikan",
+ "options": {
+ "ignoreArrayOrder": "Abaikan susunan tatasusunan"
+ },
+ "loadBothSamples": "Muat contoh",
+ "sample": "Contoh",
+ "format": "Format",
+ "clear": "Kosongkan",
+ "formatError": "Tidak boleh format: input bukan JSON yang sah",
+ "placeholder": "Tampal atau taip JSON di sini…",
+ "panels": {
+ "left": "Asal (kiri)",
+ "right": "Diubah suai (kanan)",
+ "results": "Perbezaan"
+ },
+ "summary": {
+ "added": "{count} ditambah",
+ "removed": "{count} dibuang",
+ "changed": "{count} berubah"
+ },
+ "types": {
+ "added": "Ditambah",
+ "removed": "Dibuang",
+ "changed": "Berubah"
+ },
+ "copyChanges": "Salin sebagai JSON",
+ "copied": "Disalin",
+ "empty": "Tampal JSON ke dalam kedua-dua panel untuk melihat perbezaannya.",
+ "identical": "Kedua-dua dokumen adalah sama dari segi makna."
+ },
+ "JsonpathPlayground": {
+ "title": "Ruang Uji JSONPath",
+ "subtitle": "Kueri dokumen JSON dengan JSONPath atau JMESPath dan lihat keputusan secara langsung",
+ "engineLabel": "Enjin",
+ "queryLabel": "Kueri",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Helaian rujukan",
+ "cheats": {
+ "jpAllTitles": "Semua tajuk buku",
+ "jpAllAuthors": "Setiap pengarang, di mana-mana dalam dokumen",
+ "jpFirstBook": "Buku pertama",
+ "jpLastBook": "Buku terakhir",
+ "jpSlice": "Dua buku pertama (hirisan)",
+ "jpFilterPrice": "Buku lebih murah daripada 40",
+ "jpFilterInStock": "Tajuk buku yang ada dalam stok",
+ "jpRecursive": "Semua nilai, secara rekursif",
+ "jmAllTitles": "Semua tajuk buku",
+ "jmFirstAuthor": "Pengarang buku pertama",
+ "jmLastBook": "Buku terakhir",
+ "jmSlice": "Dua buku pertama (hirisan)",
+ "jmFilterPrice": "Tajuk buku lebih murah daripada 40",
+ "jmCount": "Kira buku dalam stok",
+ "jmProjection": "Bentuk semula setiap buku menjadi objek baharu",
+ "jmSort": "Tajuk disusun mengikut harga"
+ },
+ "panels": {
+ "input": "Input JSON",
+ "results": "Keputusan"
+ },
+ "sample": "Contoh",
+ "format": "Format",
+ "clear": "Kosongkan",
+ "formatError": "Tidak boleh format: input bukan JSON yang sah",
+ "placeholder": "Tampal atau taip JSON di sini…",
+ "outputPlaceholder": "Padanan akan muncul di sini…",
+ "matchCount": "{count, plural, one {# padanan} other {# padanan}}",
+ "copy": "Salin",
+ "copied": "Disalin"
+ },
+ "KeycodeInspector": {
+ "title": "Pemeriksa Kod Kekunci",
+ "subtitle": "Tekan mana-mana kekunci untuk memeriksa peristiwa papan kekunci JavaScriptnya",
+ "capture": {
+ "prompt": "Tekan mana-mana kekunci…",
+ "ready": "Mendengar — tekan mana-mana kekunci pada papan kekunci anda",
+ "clickToFocus": "Klik di sini untuk memfokus, kemudian tekan mana-mana kekunci"
+ },
+ "hero": {
+ "spaceKey": "(Space)",
+ "keyLabel": "event.key",
+ "repeat": "ulang"
+ },
+ "copyJson": "Salin JSON",
+ "copiedJson": "JSON peristiwa disalin ke papan keratan",
+ "deprecated": "usang",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Piawai",
+ "left": "Kiri",
+ "right": "Kanan",
+ "numpad": "Pad nombor",
+ "unknown": "Tidak diketahui"
+ },
+ "history": {
+ "title": "Sejarah",
+ "clear": "Kosongkan sejarah",
+ "empty": "Tekanan kekunci akan muncul di sini."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Penjana Jadual Markdown",
+ "subtitle": "Sunting grid, tampal daripada hamparan dan salin jadual Markdown atau HTML yang tersusun kemas",
+ "addRow": "Tambah baris",
+ "addColumn": "Tambah lajur",
+ "importLabel": "Import",
+ "importPlaceholder": "Tampal TSV (dari Excel / Google Sheets) atau CSV di sini…",
+ "importButton": "Import ke dalam grid",
+ "pasteClipboard": "Tampal dari papan keratan",
+ "importEmpty": "Tiada apa untuk diimport — tampal beberapa baris dahulu",
+ "clipboardError": "Tidak dapat membaca daripada papan keratan",
+ "clear": "Kosongkan",
+ "panels": {
+ "editor": "Penyunting jadual",
+ "output": "Output"
+ },
+ "align": {
+ "left": "Jajar kiri",
+ "center": "Jajar tengah",
+ "right": "Jajar kanan"
+ },
+ "headerPlaceholder": "Pengepala",
+ "cellPlaceholder": "Sel",
+ "removeRow": "Buang baris",
+ "removeColumn": "Buang lajur",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Salin",
+ "copied": "Disalin"
+ },
+ "TimezoneConverter": {
+ "title": "Penukar Zon Waktu",
+ "subtitle": "Tukar satu detik merentas dunia untuk perancangan mesyuarat.",
+ "dateTimeLabel": "Tarikh & masa",
+ "now": "Sekarang",
+ "sourceZoneLabel": "Zon waktu sumber",
+ "selectZone": "Pilih zon",
+ "searchZone": "Cari zon waktu...",
+ "noZone": "Tiada zon waktu ditemui.",
+ "scrubLabel": "Anjak masa",
+ "reset": "Set semula",
+ "targetsLabel": "Zon sasaran",
+ "addZone": "Tambah zon",
+ "remove": "Buang",
+ "localBadge": "Setempat",
+ "invalidDate": "Masukkan tarikh dan masa yang sah.",
+ "bands": {
+ "work": "Waktu kerja (09:00–18:00)",
+ "edge": "Waktu sempadan (awal pagi / petang)",
+ "night": "Malam"
+ }
+ },
+ "TokenCounter": {
+ "title": "Pengira Token LLM",
+ "subtitle": "Kira token, visualkan cebisan dan anggarkan kos API.",
+ "inputLabel": "Teks",
+ "sample": "Contoh",
+ "clear": "Kosongkan",
+ "placeholder": "Tampal teks, kod atau JSON untuk mengira token...",
+ "encoding": "Pengekodan",
+ "loading": "Memuatkan tokenizer...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / o-series)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Token",
+ "characters": "Aksara",
+ "words": "Perkataan",
+ "ratio": "Token/perkataan",
+ "ratioSub": "anggaran"
+ },
+ "claudeRow": "Claude (anggaran)",
+ "estimate": "anggaran",
+ "viz": {
+ "title": "Visualisasi token",
+ "toggle": "Tunjuk",
+ "hint": "Togol untuk mewarnakan setiap token sebagai satu cebisan.",
+ "empty": "Masukkan teks untuk memvisualkan token.",
+ "truncated": "Memaparkan {limit} pertama daripada {total} token."
+ },
+ "cost": {
+ "title": "Kalkulator kos",
+ "disclaimer": "nilai lalai boleh disunting — sahkan harga semasa",
+ "model": "Model",
+ "expectedOutput": "Token output dijangka",
+ "customInput": "Input $/MTok",
+ "customOutput": "Output $/MTok",
+ "inputCost": "Kos input",
+ "outputCost": "Kos output",
+ "total": "Jumlah",
+ "per1000": "Setiap 1000 panggilan"
+ }
+ },
+ "WebhookTester": {
+ "title": "Penguji Webhook",
+ "subtitle": "Cipta URL sementara dan periksa permintaan webhook masuk secara langsung.",
+ "intro": {
+ "title": "Cipta URL webhook untuk mula menangkap permintaan",
+ "hint": "Anda akan mendapat URL yang tidak boleh diteka yang merekod setiap permintaan masuk. Ia luput selepas 24 jam."
+ },
+ "createBtn": "Cipta URL webhook",
+ "creating": "Mencipta…",
+ "urlLabel": "URL webhook anda",
+ "expiresIn": "Luput dalam {countdown}",
+ "copyUrlBtn": "Salin URL",
+ "copied": "Disalin",
+ "clearBtn": "Kosongkan",
+ "newBinBtn": "URL baharu",
+ "requestsTitle": "Permintaan",
+ "selectPrompt": "Pilih satu permintaan untuk memeriksa butirannya.",
+ "expiredNotice": "URL webhook ini telah luput. Cipta yang baharu untuk terus menguji.",
+ "base64Badge": "base64",
+ "truncatedBadge": "dipangkas",
+ "empty": {
+ "title": "Menunggu permintaan…",
+ "hint": "Hantar permintaan ke URL anda dan ia akan muncul di sini serta-merta. Cuba ini:"
+ },
+ "detail": {
+ "receivedAt": "Diterima",
+ "source": "IP sumber",
+ "contentType": "Content-Type",
+ "query": "Parameter kueri",
+ "headers": "Pengepala",
+ "body": "Badan",
+ "copyBody": "Salin badan",
+ "emptyBody": "Tiada badan"
+ },
+ "errors": {
+ "createFailed": "Gagal mencipta URL webhook. Sila cuba lagi.",
+ "clearFailed": "Gagal mengosongkan permintaan. Sila cuba lagi."
+ }
+ },
+ "WebsocketTester": {
+ "title": "Penguji WebSocket",
+ "subtitle": "Sambung ke titik akhir WebSocket dan hantar, terima serta periksa mesej secara langsung",
+ "urlLabel": "URL titik akhir",
+ "protocolsLabel": "Subprotokol",
+ "protocolsPlaceholder": "pilihan, dipisahkan koma",
+ "connect": "Sambung",
+ "disconnect": "Putuskan sambungan",
+ "status": {
+ "disconnected": "Terputus",
+ "connecting": "Menyambung",
+ "connected": "Tersambung",
+ "error": "Ralat"
+ },
+ "messages": {
+ "connecting": "Menyambung ke {url}…",
+ "connected": "Sambungan diwujudkan",
+ "connectedWithProtocol": "Sambungan diwujudkan (protokol: {protocol})",
+ "invalidUrl": "URL tidak sah — mesti bermula dengan ws:// atau wss://",
+ "connectFailed": "Gagal membuka sambungan",
+ "socketError": "Ralat sambungan berlaku",
+ "notConnected": "Tidak tersambung — sambung dahulu",
+ "binaryMessage": "Mesej binari diterima"
+ },
+ "log": {
+ "title": "Log Mesej",
+ "empty": "Belum ada mesej. Sambung dan mula menghantar.",
+ "copyAll": "Salin semua",
+ "download": "Muat turun sebagai .txt",
+ "clear": "Kosongkan log",
+ "prettyPrint": "Format JSON",
+ "showRaw": "Tunjuk teks mentah"
+ },
+ "composer": {
+ "placeholder": "Taip mesej untuk dihantar…",
+ "send": "Hantar",
+ "formatJson": "Format sebagai JSON",
+ "invalidJson": "Mesej bukan JSON yang sah",
+ "hint": "Cmd/Ctrl+Enter untuk hantar"
+ },
+ "ping": {
+ "label": "Auto-ping",
+ "seconds": "saat",
+ "intervalTitle": "Selang ping dalam saat",
+ "payloadPlaceholder": "muatan ping"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Carian Whois",
+ "subtitle": "Cari butiran pendaftaran bagi mana-mana domain atau alamat IP melalui RDAP",
+ "placeholder": "example.com atau 8.8.8.8",
+ "lookupBtn": "Cari",
+ "looking": "Mencari…",
+ "refreshBtn": "Muat semula",
+ "tryLabel": "Cuba:",
+ "copy": "Salin",
+ "copied": "Disalin",
+ "notAvailable": "Tidak tersedia",
+ "summary": {
+ "registrar": "Pendaftar",
+ "network": "Rangkaian"
+ },
+ "dates": {
+ "created": "Dicipta",
+ "updated": "Dikemas kini",
+ "expires": "Luput"
+ },
+ "sections": {
+ "status": "Status",
+ "nameservers": "Pelayan nama",
+ "contacts": "Kenalan",
+ "rawRdap": "RDAP mentah"
+ },
+ "contacts": {
+ "roles": "Peranan",
+ "name": "Nama",
+ "organization": "Organisasi"
+ },
+ "errors": {
+ "invalidTarget": "Masukkan nama domain atau alamat IP yang sah",
+ "lookupFailed": "Carian gagal (status {status})"
+ }
}
}
diff --git a/apps/web/messages/nb.json b/apps/web/messages/nb.json
index 0844663d..a23e3c69 100644
--- a/apps/web/messages/nb.json
+++ b/apps/web/messages/nb.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Chmod-kalkulator",
+ "codeScreenshot": "Kodeskjermbilde",
+ "cssGenerators": "CSS-generatorer",
+ "exifViewer": "EXIF-visning",
+ "faviconGenerator": "Favicon-generator",
+ "jsonDiff": "JSON-diff",
+ "jsonpathPlayground": "JSONPath-lekeplass",
+ "keycodeInspector": "Tastekode-inspektør",
+ "markdownTableGenerator": "Markdown-tabell",
+ "timezoneConverter": "Tidssonekonverterer",
+ "tokenCounter": "Token-teller",
+ "webhookTester": "Webhook-tester",
+ "websocketTester": "WebSocket-tester",
+ "whoisLookup": "Whois-oppslag"
},
"Help": {
"title": "Hjelp og dokumentasjon",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Bcrypt-generator og -kontroll",
+ "description": "Hash passord med bcrypt, og verifiser eksisterende hasher."
+ },
+ "chmodCalculator": {
+ "title": "Chmod-kalkulator",
+ "description": "Konverter Unix-tillatelser mellom oktal, symbolsk notasjon og avkrysningsbokser."
+ },
+ "codeScreenshot": {
+ "title": "Kodeskjermbilde",
+ "description": "Gjør kildekode om til vakre, delbare bilder."
+ },
+ "cssGenerators": {
+ "title": "CSS-generatorer",
+ "description": "Generatorer for box-shadow, border-radius og flytende clamp()."
+ },
+ "exifViewer": {
+ "title": "EXIF-visning og -fjerning",
+ "description": "Inspiser og fjern EXIF-metadata fra bilder lokalt."
+ },
+ "faviconGenerator": {
+ "title": "Favicon-generator",
+ "description": "Generer favicons, app-ikoner og manifest-utdrag."
+ },
+ "jsonDiff": {
+ "title": "JSON-diff",
+ "description": "Semantisk, strukturbevisst sammenligning av to JSON-dokumenter."
+ },
+ "jsonpathPlayground": {
+ "title": "JSONPath-lekeplass",
+ "description": "Spør JSON med JSONPath eller JMESPath, og se resultater live."
+ },
+ "keycodeInspector": {
+ "title": "Tastekode-inspektør",
+ "description": "Trykk på en tast for å se JavaScript-hendelseskodene og modifikatorene dens."
+ },
+ "markdownTableGenerator": {
+ "title": "Markdown-tabellgenerator",
+ "description": "Bygg Markdown- og HTML-tabeller i en regnearklignende rutenettredigerer."
+ },
+ "timezoneConverter": {
+ "title": "Tidssonekonverterer",
+ "description": "Konverter et tidspunkt på tvers av mange soner med dagsforskyvning og arbeidstidsmarkører."
+ },
+ "tokenCounter": {
+ "title": "LLM-token-teller",
+ "description": "Tell LLM-tokens, visualiser chunker, og estimer API-kostnad."
+ },
+ "webhookTester": {
+ "title": "Webhook-tester",
+ "description": "Fang opp og inspiser innkommende webhook-forespørsler i sanntid."
+ },
+ "websocketTester": {
+ "title": "WebSocket-tester",
+ "description": "Koble til WebSocket-endepunkter, og utveksle meldinger i sanntid."
+ },
+ "whoisLookup": {
+ "title": "Whois-oppslag",
+ "description": "Slå opp registreringsdetaljer for domene og IP via RDAP."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Bcrypt-generator og -kontroll",
+ "subtitle": "Hash passord med bcrypt, og verifiser hasher — alt forblir i nettleseren din",
+ "tabs": {
+ "hash": "Hash",
+ "verify": "Verifiser"
+ },
+ "showPassword": "Vis passord",
+ "hidePassword": "Skjul passord",
+ "copy": "Kopier",
+ "copied": "Kopiert",
+ "hash": {
+ "passwordLabel": "Passord",
+ "passwordPlaceholder": "Skriv inn et passord som skal hashes…",
+ "costLabel": "Kostnadsfaktor",
+ "costHint": "Høyere kostnad dobler arbeidet per trinn. 10–12 er et vanlig valg for produksjon.",
+ "slowWarning": "Kostnadsfaktorer på 13 og høyere kan ta flere sekunder å beregne.",
+ "generate": "Generer hash",
+ "generating": "Hasher…",
+ "resultLabel": "Bcrypt-hash",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "Hash-anatomi",
+ "algorithm": "algoritme",
+ "cost": "kostnad",
+ "salt": "salt (22 tegn)",
+ "hash": "hash (31 tegn)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Passord",
+ "passwordPlaceholder": "Skriv inn passordet som skal kontrolleres…",
+ "hashLabel": "Bcrypt-hash",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Skriv inn et passord og en bcrypt-hash for å sammenligne dem.",
+ "invalidHash": "Dette ser ikke ut som en gyldig bcrypt-hash.",
+ "checking": "Sammenligner…",
+ "match": "Treff — passordet produserer denne hashen.",
+ "noMatch": "Ingen treff — passordet produserer ikke denne hashen."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Chmod-kalkulator",
+ "subtitle": "Konverter Unix-filtillatelser mellom avkrysningsbokser, oktal og symbolsk notasjon",
+ "grid": {
+ "title": "Tillatelser",
+ "read": "Les",
+ "write": "Skriv",
+ "execute": "Kjør",
+ "owner": "Eier",
+ "group": "Gruppe",
+ "others": "Andre"
+ },
+ "special": {
+ "title": "Spesielle bits",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "Oktal",
+ "placeholder": "755 eller 4755",
+ "invalid": "Skriv inn 3 eller 4 oktale sifre (0–7)."
+ },
+ "symbolic": {
+ "label": "Symbolsk",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Skriv inn 9 tegn som rwxr-xr-x (s/S, t/T tillatt)."
+ },
+ "output": {
+ "title": "Kommando",
+ "copy": "Kopier",
+ "copied": "Kopiert"
+ },
+ "explain": {
+ "line": "{cls} kan {perms}.",
+ "none": "{cls} har ingen tillatelser.",
+ "setuid": "Setuid: filen kjører med eierens rettigheter.",
+ "setgid": "Setgid: filen kjører med gruppens rettigheter (nye filer i en mappe arver gruppen dens).",
+ "sticky": "Sticky: bare en fils eier kan slette eller gi den nytt navn i denne mappen."
+ },
+ "presets": {
+ "title": "Vanlige forhåndsinnstillinger",
+ "octal": "Oktal",
+ "permissions": "Tillatelser",
+ "description": "Typisk bruk",
+ "p644": "Vanlige filer: eieren skriver, alle leser.",
+ "p755": "Kjørbare filer og mapper: alle kan lese og gå inn.",
+ "p600": "Private filer: bare eieren leser og skriver.",
+ "p700": "Private kjørbare filer og mapper: bare eieren.",
+ "p664": "Gruppe-skrivbare filer.",
+ "p775": "Gruppe-skrivbare mapper og kjørbare filer.",
+ "p400": "Skrivebeskyttet for eieren (f.eks. private SSH-nøkler)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Kodeskjermbilde",
+ "subtitle": "Gjør kode om til vakre, delbare bilder",
+ "languageLabel": "Språk",
+ "autoDetect": "Automatisk gjenkjenning",
+ "backgroundLabel": "Bakgrunn",
+ "paddingLabel": "Innvendig marg",
+ "filenameLabel": "Filnavn",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "Vindusprikker",
+ "themeLabel": "Vindustema",
+ "themeDark": "Mørkt",
+ "themeLight": "Lyst",
+ "scaleLabel": "Eksportskala",
+ "copyImage": "Kopier bilde",
+ "copied": "Kopiert",
+ "exportPng": "Eksporter PNG",
+ "inputLabel": "Kode",
+ "detected": "Gjenkjent: {language}",
+ "clear": "Tøm",
+ "placeholder": "Lim inn koden din her…",
+ "previewLabel": "Forhåndsvisning",
+ "emptyPreview": "Forhåndsvisningen av koden din vises her",
+ "exportFailed": "Kunne ikke eksportere bilde",
+ "copyUnsupported": "Kopiering av bilder støttes ikke i denne nettleseren",
+ "copyFailed": "Kunne ikke kopiere bilde",
+ "backgrounds": {
+ "ocean": "Hav",
+ "sunset": "Solnedgang",
+ "candy": "Godteri",
+ "forest": "Skog",
+ "midnight": "Midnatt",
+ "gold": "Gull",
+ "aurora": "Nordlys",
+ "slate": "Skifer",
+ "transparent": "Gjennomsiktig"
+ }
+ },
+ "CssGenerators": {
+ "title": "CSS-generatorer",
+ "subtitle": "Box-shadow, border-radius og flytende clamp() — kopier CSS-en.",
+ "preview": "Forhåndsvisning",
+ "output": "CSS",
+ "copy": "Kopier",
+ "copied": "Kopiert",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "Flytende clamp()"
+ },
+ "boxShadow": {
+ "presets": "Forhåndsinnstillinger",
+ "layer": "Lag",
+ "addLayer": "Legg til lag",
+ "removeLayer": "Fjern lag",
+ "x": "X",
+ "y": "Y",
+ "blur": "Uskarphet",
+ "spread": "Spredning",
+ "color": "Farge",
+ "alpha": "Alfa",
+ "inset": "Innvendig",
+ "presetNames": {
+ "subtleCard": "Diskret kort",
+ "elevated": "Hevet",
+ "hardOffset": "Hard forskyvning",
+ "neonGlow": "Neonglød",
+ "inner": "Innvendig",
+ "layeredSmooth": "Lagdelt myk"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Modus",
+ "modes": {
+ "simple": "Per hjørne",
+ "blob": "Organisk klatt"
+ },
+ "unit": "Enhet",
+ "linkCorners": "Koble hjørner",
+ "randomize": "Tilfeldiggjør",
+ "horizontal": "Horisontale radier",
+ "vertical": "Vertikale radier",
+ "corners": {
+ "tl": "Øverst til venstre",
+ "tr": "Øverst til høyre",
+ "br": "Nederst til høyre",
+ "bl": "Nederst til venstre"
+ }
+ },
+ "clamp": {
+ "minFont": "Min. skrift (px)",
+ "maxFont": "Maks. skrift (px)",
+ "minViewport": "Min. viewport (px)",
+ "maxViewport": "Maks. viewport (px)",
+ "rootFont": "Rotskrift (px)",
+ "viewportWidth": "Simulert viewport",
+ "computedSize": "Beregnet størrelse",
+ "slope": "Stigning",
+ "intercept": "Skjæringspunkt",
+ "previewText": "Vår sære Zulu fra badeøya",
+ "invalid": "Skriv inn gyldige, ulike viewport-grenser."
+ }
+ },
+ "ExifViewer": {
+ "title": "EXIF-visning og -fjerning",
+ "subtitle": "Inspiser bilders metadata, og fjern dem i nettleseren din",
+ "dropTitle": "Slipp et bilde her",
+ "dropHint": "eller klikk for å bla — bildet ditt forlater aldri enheten din",
+ "browse": "Velg bilde",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Leser metadata…",
+ "parseFailed": "Kunne ikke lese dette bildet",
+ "clear": "Fjern bilde",
+ "gpsWarning": "Inneholder GPS-posisjon",
+ "openMap": "Vis på OpenStreetMap",
+ "noMetadata": "Ingen innebygde metadata ble funnet i dette bildet.",
+ "facts": {
+ "name": "Navn",
+ "type": "Type",
+ "dimensions": "Dimensjoner",
+ "size": "Størrelse"
+ },
+ "groups": {
+ "camera": "Kamera",
+ "exposure": "Eksponering",
+ "timestamps": "Tidsstempler",
+ "gps": "GPS-posisjon",
+ "software": "Programvare og annet"
+ },
+ "gpsCard": {
+ "latitude": "Breddegrad",
+ "longitude": "Lengdegrad",
+ "altitude": "Høyde"
+ },
+ "raw": {
+ "title": "Alle metadata",
+ "copyJson": "Kopier JSON",
+ "copied": "Kopiert"
+ },
+ "remove": {
+ "title": "Fjern metadata",
+ "jpegDescription": "Fjern EXIF, XMP, IPTC og kommentarer tapsfritt uten å komprimere bildet på nytt.",
+ "reencodeDescription": "Kod om bildet for å fjerne innebygde metadata. Dette komprimerer pikslene på nytt.",
+ "keepIcc": "Behold fargeprofil (ICC)",
+ "stripButton": "Fjern metadata",
+ "reencodeButton": "Kod om og fjern",
+ "unsupported": "Fjerning av metadata støttes ikke for denne filtypen.",
+ "failed": "Kunne ikke fjerne metadata",
+ "before": "Før",
+ "after": "Etter",
+ "download": "Last ned renset bilde"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Favicon-generator",
+ "subtitle": "Lag favicons og app-ikoner fra ett bilde",
+ "dropTitle": "Slipp et bilde her",
+ "dropHint": "PNG, JPEG eller SVG — klikk for å bla",
+ "browse": "Velg bilde",
+ "clear": "Fjern bilde",
+ "backgroundLabel": "Bakgrunn",
+ "transparent": "Gjennomsiktig",
+ "solidColor": "Heldekkende",
+ "cornerLabel": "Hjørneform",
+ "cornerNone": "Firkantet",
+ "cornerRounded": "Avrundet",
+ "cornerCircle": "Sirkel",
+ "maskPaddingLabel": "Maskerbar marg",
+ "maskable": "maskable",
+ "previewLabel": "Forhåndsvisning",
+ "copy": "Kopier",
+ "headSnippet": "HTML head-utdrag",
+ "downloadIco": "Last ned favicon.ico",
+ "downloadZip": "Last ned alle (.zip)"
+ },
+ "JsonDiff": {
+ "title": "JSON-diff",
+ "subtitle": "Semantisk, strukturbevisst sammenligning av to JSON-dokumenter — nøkkelrekkefølge og formatering ignoreres",
+ "options": {
+ "ignoreArrayOrder": "Ignorer array-rekkefølge"
+ },
+ "loadBothSamples": "Last eksempler",
+ "sample": "Eksempel",
+ "format": "Formater",
+ "clear": "Tøm",
+ "formatError": "Kan ikke formatere: inndata er ikke gyldig JSON",
+ "placeholder": "Lim inn eller skriv JSON her…",
+ "panels": {
+ "left": "Original (venstre)",
+ "right": "Endret (høyre)",
+ "results": "Forskjeller"
+ },
+ "summary": {
+ "added": "{count} lagt til",
+ "removed": "{count} fjernet",
+ "changed": "{count} endret"
+ },
+ "types": {
+ "added": "Lagt til",
+ "removed": "Fjernet",
+ "changed": "Endret"
+ },
+ "copyChanges": "Kopier som JSON",
+ "copied": "Kopiert",
+ "empty": "Lim inn JSON i begge panelene for å se forskjellene deres.",
+ "identical": "De to dokumentene er semantisk identiske."
+ },
+ "JsonpathPlayground": {
+ "title": "JSONPath-lekeplass",
+ "subtitle": "Spør JSON-dokumenter med JSONPath eller JMESPath, og se resultater live",
+ "engineLabel": "Motor",
+ "queryLabel": "Spørring",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Jukselapp",
+ "cheats": {
+ "jpAllTitles": "Alle boktitler",
+ "jpAllAuthors": "Hver forfatter, hvor som helst i dokumentet",
+ "jpFirstBook": "Første bok",
+ "jpLastBook": "Siste bok",
+ "jpSlice": "De to første bøkene (utsnitt)",
+ "jpFilterPrice": "Bøker billigere enn 40",
+ "jpFilterInStock": "Titler på bøker på lager",
+ "jpRecursive": "Alle verdier, rekursivt",
+ "jmAllTitles": "Alle boktitler",
+ "jmFirstAuthor": "Forfatter av den første boken",
+ "jmLastBook": "Siste bok",
+ "jmSlice": "De to første bøkene (utsnitt)",
+ "jmFilterPrice": "Titler på bøker billigere enn 40",
+ "jmCount": "Antall bøker på lager",
+ "jmProjection": "Omform hver bok til et nytt objekt",
+ "jmSort": "Titler sortert etter pris"
+ },
+ "panels": {
+ "input": "JSON-inndata",
+ "results": "Resultater"
+ },
+ "sample": "Eksempel",
+ "format": "Formater",
+ "clear": "Tøm",
+ "formatError": "Kan ikke formatere: inndata er ikke gyldig JSON",
+ "placeholder": "Lim inn eller skriv JSON her…",
+ "outputPlaceholder": "Treff vises her…",
+ "matchCount": "{count, plural, one {# treff} other {# treff}}",
+ "copy": "Kopier",
+ "copied": "Kopiert"
+ },
+ "KeycodeInspector": {
+ "title": "Tastekode-inspektør",
+ "subtitle": "Trykk på en hvilken som helst tast for å inspisere JavaScript-tastaturhendelsen dens",
+ "capture": {
+ "prompt": "Trykk på en hvilken som helst tast…",
+ "ready": "Lytter — trykk på en hvilken som helst tast på tastaturet",
+ "clickToFocus": "Klikk her for å fokusere, og trykk deretter på en hvilken som helst tast"
+ },
+ "hero": {
+ "spaceKey": "(Mellomrom)",
+ "keyLabel": "event.key",
+ "repeat": "gjentakelse"
+ },
+ "copyJson": "Kopier JSON",
+ "copiedJson": "Hendelses-JSON kopiert til utklippstavlen",
+ "deprecated": "utdatert",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Standard",
+ "left": "Venstre",
+ "right": "Høyre",
+ "numpad": "Numerisk tastatur",
+ "unknown": "Ukjent"
+ },
+ "history": {
+ "title": "Historikk",
+ "clear": "Tøm historikk",
+ "empty": "Tastetrykk vises her."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Markdown-tabellgenerator",
+ "subtitle": "Rediger et rutenett, lim inn fra et regneark, og kopier en pent justert Markdown- eller HTML-tabell",
+ "addRow": "Legg til rad",
+ "addColumn": "Legg til kolonne",
+ "importLabel": "Importer",
+ "importPlaceholder": "Lim inn TSV (fra Excel / Google Sheets) eller CSV her…",
+ "importButton": "Importer til rutenett",
+ "pasteClipboard": "Lim inn fra utklippstavlen",
+ "importEmpty": "Ingenting å importere — lim inn noen rader først",
+ "clipboardError": "Kunne ikke lese fra utklippstavlen",
+ "clear": "Tøm",
+ "panels": {
+ "editor": "Tabellredigerer",
+ "output": "Utdata"
+ },
+ "align": {
+ "left": "Venstrejuster",
+ "center": "Midtstill",
+ "right": "Høyrejuster"
+ },
+ "headerPlaceholder": "Overskrift",
+ "cellPlaceholder": "Celle",
+ "removeRow": "Fjern rad",
+ "removeColumn": "Fjern kolonne",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Kopier",
+ "copied": "Kopiert"
+ },
+ "TimezoneConverter": {
+ "title": "Tidssonekonverterer",
+ "subtitle": "Konverter ett øyeblikk på tvers av verden for møteplanlegging.",
+ "dateTimeLabel": "Dato og tid",
+ "now": "Nå",
+ "sourceZoneLabel": "Kilde-tidssone",
+ "selectZone": "Velg sone",
+ "searchZone": "Søk tidssoner...",
+ "noZone": "Ingen tidssone funnet.",
+ "scrubLabel": "Forskyv tid",
+ "reset": "Tilbakestill",
+ "targetsLabel": "Målsoner",
+ "addZone": "Legg til sone",
+ "remove": "Fjern",
+ "localBadge": "Lokal",
+ "invalidDate": "Skriv inn en gyldig dato og tid.",
+ "bands": {
+ "work": "Arbeidstid (09:00–18:00)",
+ "edge": "Randtimer (tidlig / kveld)",
+ "night": "Natt"
+ }
+ },
+ "TokenCounter": {
+ "title": "LLM-token-teller",
+ "subtitle": "Tell tokens, visualiser chunker, og estimer API-kostnad.",
+ "inputLabel": "Tekst",
+ "sample": "Eksempel",
+ "clear": "Tøm",
+ "placeholder": "Lim inn tekst, kode eller JSON for å telle tokens...",
+ "encoding": "Koding",
+ "loading": "Laster tokenizer...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / o-serien)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Tokens",
+ "characters": "Tegn",
+ "words": "Ord",
+ "ratio": "Tokens/ord",
+ "ratioSub": "omtrent"
+ },
+ "claudeRow": "Claude (omtrentlig)",
+ "estimate": "estimat",
+ "viz": {
+ "title": "Token-visualisering",
+ "toggle": "Vis",
+ "hint": "Slå på for å farge hvert token som en chunk.",
+ "empty": "Skriv inn tekst for å visualisere tokens.",
+ "truncated": "Viser de første {limit} av {total} tokens."
+ },
+ "cost": {
+ "title": "Kostnadskalkulator",
+ "disclaimer": "redigerbare standardverdier — verifiser gjeldende priser",
+ "model": "Modell",
+ "expectedOutput": "Forventede output-tokens",
+ "customInput": "Input $/MTok",
+ "customOutput": "Output $/MTok",
+ "inputCost": "Input-kostnad",
+ "outputCost": "Output-kostnad",
+ "total": "Totalt",
+ "per1000": "Per 1000 kall"
+ }
+ },
+ "WebhookTester": {
+ "title": "Webhook-tester",
+ "subtitle": "Opprett en midlertidig URL, og inspiser innkommende webhook-forespørsler live.",
+ "intro": {
+ "title": "Opprett en webhook-URL for å begynne å fange opp forespørsler",
+ "hint": "Du får en ugjettbar URL som registrerer hver innkommende forespørsel. Den utløper etter 24 timer."
+ },
+ "createBtn": "Opprett webhook-URL",
+ "creating": "Oppretter…",
+ "urlLabel": "Din webhook-URL",
+ "expiresIn": "Utløper om {countdown}",
+ "copyUrlBtn": "Kopier URL",
+ "copied": "Kopiert",
+ "clearBtn": "Tøm",
+ "newBinBtn": "Ny URL",
+ "requestsTitle": "Forespørsler",
+ "selectPrompt": "Velg en forespørsel for å inspisere detaljene.",
+ "expiredNotice": "Denne webhook-URL-en har utløpt. Opprett en ny for å fortsette å teste.",
+ "base64Badge": "base64",
+ "truncatedBadge": "avkortet",
+ "empty": {
+ "title": "Venter på forespørsler…",
+ "hint": "Send en forespørsel til URL-en din, så vises den her umiddelbart. Prøv dette:"
+ },
+ "detail": {
+ "receivedAt": "Mottatt",
+ "source": "Kilde-IP",
+ "contentType": "Content-Type",
+ "query": "Spørringsparametere",
+ "headers": "Headere",
+ "body": "Body",
+ "copyBody": "Kopier body",
+ "emptyBody": "Ingen body"
+ },
+ "errors": {
+ "createFailed": "Kunne ikke opprette webhook-URL. Prøv igjen.",
+ "clearFailed": "Kunne ikke tømme forespørsler. Prøv igjen."
+ }
+ },
+ "WebsocketTester": {
+ "title": "WebSocket-tester",
+ "subtitle": "Koble til et WebSocket-endepunkt, og send, motta og inspiser meldinger live",
+ "urlLabel": "Endepunkt-URL",
+ "protocolsLabel": "Underprotokoller",
+ "protocolsPlaceholder": "valgfritt, kommaseparert",
+ "connect": "Koble til",
+ "disconnect": "Koble fra",
+ "status": {
+ "disconnected": "Frakoblet",
+ "connecting": "Kobler til",
+ "connected": "Tilkoblet",
+ "error": "Feil"
+ },
+ "messages": {
+ "connecting": "Kobler til {url}…",
+ "connected": "Tilkobling opprettet",
+ "connectedWithProtocol": "Tilkobling opprettet (protokoll: {protocol})",
+ "invalidUrl": "Ugyldig URL — må begynne med ws:// eller wss://",
+ "connectFailed": "Kunne ikke åpne tilkoblingen",
+ "socketError": "Det oppstod en tilkoblingsfeil",
+ "notConnected": "Ikke tilkoblet — koble til først",
+ "binaryMessage": "Mottok en binær melding"
+ },
+ "log": {
+ "title": "Meldingslogg",
+ "empty": "Ingen meldinger ennå. Koble til, og begynn å sende.",
+ "copyAll": "Kopier alle",
+ "download": "Last ned som .txt",
+ "clear": "Tøm logg",
+ "prettyPrint": "Formater JSON pent",
+ "showRaw": "Vis rå tekst"
+ },
+ "composer": {
+ "placeholder": "Skriv en melding som skal sendes…",
+ "send": "Send",
+ "formatJson": "Formater som JSON",
+ "invalidJson": "Meldingen er ikke gyldig JSON",
+ "hint": "Cmd/Ctrl+Enter for å sende"
+ },
+ "ping": {
+ "label": "Auto-ping",
+ "seconds": "sek",
+ "intervalTitle": "Ping-intervall i sekunder",
+ "payloadPlaceholder": "ping-payload"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Whois-oppslag",
+ "subtitle": "Slå opp registreringsdetaljer for et hvilket som helst domene eller IP-adresse via RDAP",
+ "placeholder": "example.com eller 8.8.8.8",
+ "lookupBtn": "Slå opp",
+ "looking": "Slår opp…",
+ "refreshBtn": "Oppdater",
+ "tryLabel": "Prøv:",
+ "copy": "Kopier",
+ "copied": "Kopiert",
+ "notAvailable": "Ikke tilgjengelig",
+ "summary": {
+ "registrar": "Registrar",
+ "network": "Nettverk"
+ },
+ "dates": {
+ "created": "Opprettet",
+ "updated": "Oppdatert",
+ "expires": "Utløper"
+ },
+ "sections": {
+ "status": "Status",
+ "nameservers": "Navnetjenere",
+ "contacts": "Kontakter",
+ "rawRdap": "Rå RDAP"
+ },
+ "contacts": {
+ "roles": "Roller",
+ "name": "Navn",
+ "organization": "Organisasjon"
+ },
+ "errors": {
+ "invalidTarget": "Skriv inn et gyldig domenenavn eller IP-adresse",
+ "lookupFailed": "Oppslag mislyktes (status {status})"
+ }
}
}
diff --git a/apps/web/messages/nl.json b/apps/web/messages/nl.json
index 18e20392..a99f1b76 100644
--- a/apps/web/messages/nl.json
+++ b/apps/web/messages/nl.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Chmod-calculator",
+ "codeScreenshot": "Code-screenshot",
+ "cssGenerators": "CSS-generatoren",
+ "exifViewer": "EXIF-viewer",
+ "faviconGenerator": "Favicon-generator",
+ "jsonDiff": "JSON-diff",
+ "jsonpathPlayground": "JSONPath-playground",
+ "keycodeInspector": "Keycode-inspecteur",
+ "markdownTableGenerator": "Markdown-tabel",
+ "timezoneConverter": "Tijdzone-converter",
+ "tokenCounter": "Token-teller",
+ "webhookTester": "Webhook-tester",
+ "websocketTester": "WebSocket-tester",
+ "whoisLookup": "Whois-opzoeking"
},
"Help": {
"title": "Help en documentatie",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Bcrypt-generator en -controle",
+ "description": "Hash wachtwoorden met bcrypt en verifieer bestaande hashes."
+ },
+ "chmodCalculator": {
+ "title": "Chmod-calculator",
+ "description": "Zet Unix-rechten om tussen octaal, symbolisch en selectievakjes."
+ },
+ "codeScreenshot": {
+ "title": "Code-screenshot",
+ "description": "Zet broncode om in mooie, deelbare afbeeldingen."
+ },
+ "cssGenerators": {
+ "title": "CSS-generatoren",
+ "description": "Generatoren voor box-shadow, border-radius en vloeiende clamp()."
+ },
+ "exifViewer": {
+ "title": "EXIF-viewer en -verwijderaar",
+ "description": "Inspecteer en verwijder EXIF-metadata van foto's lokaal."
+ },
+ "faviconGenerator": {
+ "title": "Favicon-generator",
+ "description": "Genereer favicons, app-iconen en manifest-snippets."
+ },
+ "jsonDiff": {
+ "title": "JSON-diff",
+ "description": "Semantische, structuurbewuste vergelijking van twee JSON-documenten."
+ },
+ "jsonpathPlayground": {
+ "title": "JSONPath-playground",
+ "description": "Bevraag JSON met JSONPath of JMESPath en zie de resultaten live."
+ },
+ "keycodeInspector": {
+ "title": "Keycode-inspecteur",
+ "description": "Druk op een toets om de JavaScript-event-codes en modifiers te zien."
+ },
+ "markdownTableGenerator": {
+ "title": "Markdown-tabelgenerator",
+ "description": "Maak Markdown- en HTML-tabellen in een spreadsheetachtige rasteditor."
+ },
+ "timezoneConverter": {
+ "title": "Tijdzone-converter",
+ "description": "Zet een tijd om naar veel zones, met aanwijzingen voor dagverschuiving en werktijden."
+ },
+ "tokenCounter": {
+ "title": "LLM-token-teller",
+ "description": "Tel LLM-tokens, visualiseer chunks en schat de API-kosten."
+ },
+ "webhookTester": {
+ "title": "Webhook-tester",
+ "description": "Vang inkomende webhook-verzoeken op en inspecteer ze in realtime."
+ },
+ "websocketTester": {
+ "title": "WebSocket-tester",
+ "description": "Verbind met WebSocket-endpoints en wissel berichten uit in realtime."
+ },
+ "whoisLookup": {
+ "title": "Whois-opzoeking",
+ "description": "Zoek registratiegegevens van een domein of IP op via RDAP."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Bcrypt-generator en -controle",
+ "subtitle": "Hash wachtwoorden met bcrypt en verifieer hashes — alles blijft in je browser",
+ "tabs": {
+ "hash": "Hashen",
+ "verify": "Verifiëren"
+ },
+ "showPassword": "Wachtwoord tonen",
+ "hidePassword": "Wachtwoord verbergen",
+ "copy": "Kopiëren",
+ "copied": "Gekopieerd",
+ "hash": {
+ "passwordLabel": "Wachtwoord",
+ "passwordPlaceholder": "Voer een wachtwoord in om te hashen…",
+ "costLabel": "Kostenfactor",
+ "costHint": "Een hogere kostenfactor verdubbelt het werk per stap. 10-12 is een gangbare keuze voor productie.",
+ "slowWarning": "Kostenfactoren van 13 en hoger kunnen enkele seconden duren om te berekenen.",
+ "generate": "Hash genereren",
+ "generating": "Bezig met hashen…",
+ "resultLabel": "Bcrypt-hash",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "Opbouw van de hash",
+ "algorithm": "algoritme",
+ "cost": "kosten",
+ "salt": "salt (22 tekens)",
+ "hash": "hash (31 tekens)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Wachtwoord",
+ "passwordPlaceholder": "Voer het te controleren wachtwoord in…",
+ "hashLabel": "Bcrypt-hash",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Voer een wachtwoord en een bcrypt-hash in om ze te vergelijken.",
+ "invalidHash": "Dit lijkt geen geldige bcrypt-hash.",
+ "checking": "Bezig met vergelijken…",
+ "match": "Komt overeen — het wachtwoord levert deze hash op.",
+ "noMatch": "Komt niet overeen — het wachtwoord levert deze hash niet op."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Chmod-calculator",
+ "subtitle": "Zet Unix-bestandsrechten om tussen selectievakjes, octaal en symbolische notatie",
+ "grid": {
+ "title": "Rechten",
+ "read": "Lezen",
+ "write": "Schrijven",
+ "execute": "Uitvoeren",
+ "owner": "Eigenaar",
+ "group": "Groep",
+ "others": "Anderen"
+ },
+ "special": {
+ "title": "Speciale bits",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "Octaal",
+ "placeholder": "755 of 4755",
+ "invalid": "Voer 3 of 4 octale cijfers (0-7) in."
+ },
+ "symbolic": {
+ "label": "Symbolisch",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Voer 9 tekens in zoals rwxr-xr-x (s/S, t/T toegestaan)."
+ },
+ "output": {
+ "title": "Commando",
+ "copy": "Kopiëren",
+ "copied": "Gekopieerd"
+ },
+ "explain": {
+ "line": "{cls} kan {perms}.",
+ "none": "{cls} heeft geen rechten.",
+ "setuid": "Setuid: het bestand draait met de rechten van de eigenaar.",
+ "setgid": "Setgid: het bestand draait met de rechten van zijn groep (nieuwe bestanden in een map erven de groep ervan).",
+ "sticky": "Sticky: alleen de eigenaar van een bestand kan het verwijderen of hernoemen binnen deze map."
+ },
+ "presets": {
+ "title": "Veelgebruikte presets",
+ "octal": "Octaal",
+ "permissions": "Rechten",
+ "description": "Typisch gebruik",
+ "p644": "Gewone bestanden: eigenaar schrijft, iedereen leest.",
+ "p755": "Uitvoerbare bestanden en mappen: iedereen kan lezen en openen.",
+ "p600": "Privébestanden: alleen de eigenaar leest en schrijft.",
+ "p700": "Privé uitvoerbare bestanden en mappen: alleen de eigenaar.",
+ "p664": "Bestanden die door de groep beschrijfbaar zijn.",
+ "p775": "Mappen en uitvoerbare bestanden die door de groep beschrijfbaar zijn.",
+ "p400": "Alleen-lezen voor de eigenaar (bijv. SSH-privésleutels)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Code-screenshot",
+ "subtitle": "Zet code om in mooie, deelbare afbeeldingen",
+ "languageLabel": "Taal",
+ "autoDetect": "Automatisch detecteren",
+ "backgroundLabel": "Achtergrond",
+ "paddingLabel": "Opvulling",
+ "filenameLabel": "Bestandsnaam",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "Vensterstippen",
+ "themeLabel": "Vensterthema",
+ "themeDark": "Donker",
+ "themeLight": "Licht",
+ "scaleLabel": "Exportschaal",
+ "copyImage": "Afbeelding kopiëren",
+ "copied": "Gekopieerd",
+ "exportPng": "PNG exporteren",
+ "inputLabel": "Code",
+ "detected": "Gedetecteerd: {language}",
+ "clear": "Wissen",
+ "placeholder": "Plak hier je code…",
+ "previewLabel": "Voorbeeld",
+ "emptyPreview": "Het voorbeeld van je code verschijnt hier",
+ "exportFailed": "Afbeelding exporteren mislukt",
+ "copyUnsupported": "Afbeeldingen kopiëren wordt niet ondersteund in deze browser",
+ "copyFailed": "Afbeelding kopiëren mislukt",
+ "backgrounds": {
+ "ocean": "Oceaan",
+ "sunset": "Zonsondergang",
+ "candy": "Snoep",
+ "forest": "Bos",
+ "midnight": "Middernacht",
+ "gold": "Goud",
+ "aurora": "Poollicht",
+ "slate": "Leisteen",
+ "transparent": "Transparant"
+ }
+ },
+ "CssGenerators": {
+ "title": "CSS-generatoren",
+ "subtitle": "Box-shadow, border-radius en vloeiende clamp() — kopieer de CSS.",
+ "preview": "Voorbeeld",
+ "output": "CSS",
+ "copy": "Kopiëren",
+ "copied": "Gekopieerd",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "Vloeiende clamp()"
+ },
+ "boxShadow": {
+ "presets": "Presets",
+ "layer": "Laag",
+ "addLayer": "Laag toevoegen",
+ "removeLayer": "Laag verwijderen",
+ "x": "X",
+ "y": "Y",
+ "blur": "Vervaging",
+ "spread": "Spreiding",
+ "color": "Kleur",
+ "alpha": "Alfa",
+ "inset": "Binnen",
+ "presetNames": {
+ "subtleCard": "Subtiele kaart",
+ "elevated": "Verhoogd",
+ "hardOffset": "Harde verschuiving",
+ "neonGlow": "Neongloed",
+ "inner": "Binnen",
+ "layeredSmooth": "Gelaagd zacht"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Modus",
+ "modes": {
+ "simple": "Per hoek",
+ "blob": "Organische blob"
+ },
+ "unit": "Eenheid",
+ "linkCorners": "Hoeken koppelen",
+ "randomize": "Willekeurig",
+ "horizontal": "Horizontale radii",
+ "vertical": "Verticale radii",
+ "corners": {
+ "tl": "Linksboven",
+ "tr": "Rechtsboven",
+ "br": "Rechtsonder",
+ "bl": "Linksonder"
+ }
+ },
+ "clamp": {
+ "minFont": "Min. lettergrootte (px)",
+ "maxFont": "Max. lettergrootte (px)",
+ "minViewport": "Min. viewport (px)",
+ "maxViewport": "Max. viewport (px)",
+ "rootFont": "Root-lettergrootte (px)",
+ "viewportWidth": "Gesimuleerde viewport",
+ "computedSize": "Berekende grootte",
+ "slope": "Helling",
+ "intercept": "Snijpunt",
+ "previewText": "De snelle bruine vos",
+ "invalid": "Voer geldige, verschillende viewport-grenzen in."
+ }
+ },
+ "ExifViewer": {
+ "title": "EXIF-viewer en -verwijderaar",
+ "subtitle": "Inspecteer foto-metadata en verwijder ze in je browser",
+ "dropTitle": "Sleep hier een afbeelding",
+ "dropHint": "of klik om te bladeren — je afbeelding verlaat nooit je apparaat",
+ "browse": "Afbeelding kiezen",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Metadata lezen…",
+ "parseFailed": "Deze afbeelding kon niet worden gelezen",
+ "clear": "Afbeelding verwijderen",
+ "gpsWarning": "Bevat GPS-locatie",
+ "openMap": "Bekijk op OpenStreetMap",
+ "noMetadata": "Er is geen ingesloten metadata gevonden in deze afbeelding.",
+ "facts": {
+ "name": "Naam",
+ "type": "Type",
+ "dimensions": "Afmetingen",
+ "size": "Grootte"
+ },
+ "groups": {
+ "camera": "Camera",
+ "exposure": "Belichting",
+ "timestamps": "Tijdstempels",
+ "gps": "GPS-locatie",
+ "software": "Software en overig"
+ },
+ "gpsCard": {
+ "latitude": "Breedtegraad",
+ "longitude": "Lengtegraad",
+ "altitude": "Hoogte"
+ },
+ "raw": {
+ "title": "Alle metadata",
+ "copyJson": "JSON kopiëren",
+ "copied": "Gekopieerd"
+ },
+ "remove": {
+ "title": "Metadata verwijderen",
+ "jpegDescription": "Verwijder EXIF, XMP, IPTC en opmerkingen zonder verlies, zonder de afbeelding opnieuw te comprimeren.",
+ "reencodeDescription": "Codeer de afbeelding opnieuw om ingesloten metadata te verwijderen. Hierbij worden de pixels opnieuw gecomprimeerd.",
+ "keepIcc": "Kleurprofiel (ICC) behouden",
+ "stripButton": "Metadata verwijderen",
+ "reencodeButton": "Opnieuw coderen en wissen",
+ "unsupported": "Metadata verwijderen wordt niet ondersteund voor dit bestandstype.",
+ "failed": "Metadata verwijderen mislukt",
+ "before": "Voor",
+ "after": "Na",
+ "download": "Opgeschoonde afbeelding downloaden"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Favicon-generator",
+ "subtitle": "Maak favicons en app-iconen uit één afbeelding",
+ "dropTitle": "Sleep hier een afbeelding",
+ "dropHint": "PNG, JPEG of SVG — klik om te bladeren",
+ "browse": "Afbeelding kiezen",
+ "clear": "Afbeelding verwijderen",
+ "backgroundLabel": "Achtergrond",
+ "transparent": "Transparant",
+ "solidColor": "Effen",
+ "cornerLabel": "Hoekvorm",
+ "cornerNone": "Vierkant",
+ "cornerRounded": "Afgerond",
+ "cornerCircle": "Cirkel",
+ "maskPaddingLabel": "Maskable-opvulling",
+ "maskable": "maskable",
+ "previewLabel": "Voorbeeld",
+ "copy": "Kopiëren",
+ "headSnippet": "HTML-head-snippet",
+ "downloadIco": "favicon.ico downloaden",
+ "downloadZip": "Alles downloaden (.zip)"
+ },
+ "JsonDiff": {
+ "title": "JSON-diff",
+ "subtitle": "Semantische, structuurbewuste vergelijking van twee JSON-documenten — sleutelvolgorde en opmaak worden genegeerd",
+ "options": {
+ "ignoreArrayOrder": "Arrayvolgorde negeren"
+ },
+ "loadBothSamples": "Voorbeelden laden",
+ "sample": "Voorbeeld laden",
+ "format": "Formatteren",
+ "clear": "Wissen",
+ "formatError": "Kan niet formatteren: de invoer is geen geldige JSON",
+ "placeholder": "Plak of typ hier JSON…",
+ "panels": {
+ "left": "Origineel (links)",
+ "right": "Gewijzigd (rechts)",
+ "results": "Verschillen"
+ },
+ "summary": {
+ "added": "{count} toegevoegd",
+ "removed": "{count} verwijderd",
+ "changed": "{count} gewijzigd"
+ },
+ "types": {
+ "added": "Toegevoegd",
+ "removed": "Verwijderd",
+ "changed": "Gewijzigd"
+ },
+ "copyChanges": "Kopiëren als JSON",
+ "copied": "Gekopieerd",
+ "empty": "Plak JSON in beide panelen om hun verschillen te zien.",
+ "identical": "De twee documenten zijn semantisch identiek."
+ },
+ "JsonpathPlayground": {
+ "title": "JSONPath-playground",
+ "subtitle": "Bevraag JSON-documenten met JSONPath of JMESPath en zie de resultaten live",
+ "engineLabel": "Engine",
+ "queryLabel": "Query",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Spiekbriefje",
+ "cheats": {
+ "jpAllTitles": "Alle boektitels",
+ "jpAllAuthors": "Elke auteur, overal in het document",
+ "jpFirstBook": "Eerste boek",
+ "jpLastBook": "Laatste boek",
+ "jpSlice": "De eerste twee boeken (slice)",
+ "jpFilterPrice": "Boeken goedkoper dan 40",
+ "jpFilterInStock": "Titels van boeken op voorraad",
+ "jpRecursive": "Alle waarden, recursief",
+ "jmAllTitles": "Alle boektitels",
+ "jmFirstAuthor": "Auteur van het eerste boek",
+ "jmLastBook": "Laatste boek",
+ "jmSlice": "De eerste twee boeken (slice)",
+ "jmFilterPrice": "Titels van boeken goedkoper dan 40",
+ "jmCount": "Boeken op voorraad tellen",
+ "jmProjection": "Elk boek omvormen tot een nieuw object",
+ "jmSort": "Titels gesorteerd op prijs"
+ },
+ "panels": {
+ "input": "JSON-invoer",
+ "results": "Resultaten"
+ },
+ "sample": "Voorbeeld laden",
+ "format": "Formatteren",
+ "clear": "Wissen",
+ "formatError": "Kan niet formatteren: de invoer is geen geldige JSON",
+ "placeholder": "Plak of typ hier JSON…",
+ "outputPlaceholder": "Overeenkomsten verschijnen hier…",
+ "matchCount": "{count, plural, one {# overeenkomst} other {# overeenkomsten}}",
+ "copy": "Kopiëren",
+ "copied": "Gekopieerd"
+ },
+ "KeycodeInspector": {
+ "title": "Keycode-inspecteur",
+ "subtitle": "Druk op een willekeurige toets om het JavaScript-toetsenbordevent te inspecteren",
+ "capture": {
+ "prompt": "Druk op een willekeurige toets…",
+ "ready": "Aan het luisteren — druk op een willekeurige toets op je toetsenbord",
+ "clickToFocus": "Klik hier om te focussen en druk daarna op een willekeurige toets"
+ },
+ "hero": {
+ "spaceKey": "(Spatie)",
+ "keyLabel": "event.key",
+ "repeat": "herhaling"
+ },
+ "copyJson": "JSON kopiëren",
+ "copiedJson": "Event-JSON naar het klembord gekopieerd",
+ "deprecated": "verouderd",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Standaard",
+ "left": "Links",
+ "right": "Rechts",
+ "numpad": "Numeriek toetsenblok",
+ "unknown": "Onbekend"
+ },
+ "history": {
+ "title": "Geschiedenis",
+ "clear": "Geschiedenis wissen",
+ "empty": "Toetsaanslagen verschijnen hier."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Markdown-tabelgenerator",
+ "subtitle": "Bewerk een raster, plak uit een spreadsheet en kopieer een netjes uitgelijnde Markdown- of HTML-tabel",
+ "addRow": "Rij toevoegen",
+ "addColumn": "Kolom toevoegen",
+ "importLabel": "Importeren",
+ "importPlaceholder": "Plak hier TSV (uit Excel / Google Sheets) of CSV…",
+ "importButton": "Importeren in raster",
+ "pasteClipboard": "Plakken van klembord",
+ "importEmpty": "Niets om te importeren — plak eerst een paar rijen",
+ "clipboardError": "Kon niet van het klembord lezen",
+ "clear": "Wissen",
+ "panels": {
+ "editor": "Tabeleditor",
+ "output": "Uitvoer"
+ },
+ "align": {
+ "left": "Links uitlijnen",
+ "center": "Centreren",
+ "right": "Rechts uitlijnen"
+ },
+ "headerPlaceholder": "Koptekst",
+ "cellPlaceholder": "Cel",
+ "removeRow": "Rij verwijderen",
+ "removeColumn": "Kolom verwijderen",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Kopiëren",
+ "copied": "Gekopieerd"
+ },
+ "TimezoneConverter": {
+ "title": "Tijdzone-converter",
+ "subtitle": "Zet één moment om over de hele wereld voor het plannen van vergaderingen.",
+ "dateTimeLabel": "Datum en tijd",
+ "now": "Nu",
+ "sourceZoneLabel": "Brontijdzone",
+ "selectZone": "Zone selecteren",
+ "searchZone": "Tijdzones zoeken...",
+ "noZone": "Geen tijdzone gevonden.",
+ "scrubLabel": "Tijd verschuiven",
+ "reset": "Opnieuw instellen",
+ "targetsLabel": "Doeltijdzones",
+ "addZone": "Zone toevoegen",
+ "remove": "Verwijderen",
+ "localBadge": "Lokaal",
+ "invalidDate": "Voer een geldige datum en tijd in.",
+ "bands": {
+ "work": "Werktijden (09:00–18:00)",
+ "edge": "Randuren (vroeg / avond)",
+ "night": "Nacht"
+ }
+ },
+ "TokenCounter": {
+ "title": "LLM-token-teller",
+ "subtitle": "Tel tokens, visualiseer chunks en schat de API-kosten.",
+ "inputLabel": "Tekst",
+ "sample": "Voorbeeld laden",
+ "clear": "Wissen",
+ "placeholder": "Plak tekst, code of JSON om tokens te tellen...",
+ "encoding": "Codering",
+ "loading": "Tokenizer laden...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / o-serie)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Tokens",
+ "characters": "Tekens",
+ "words": "Woorden",
+ "ratio": "Tokens/woord",
+ "ratioSub": "ong."
+ },
+ "claudeRow": "Claude (bij benadering)",
+ "estimate": "schatting",
+ "viz": {
+ "title": "Token-visualisatie",
+ "toggle": "Tonen",
+ "hint": "Schakel in om elk token als een chunk in te kleuren.",
+ "empty": "Voer tekst in om tokens te visualiseren.",
+ "truncated": "De eerste {limit} van {total} tokens worden getoond."
+ },
+ "cost": {
+ "title": "Kostencalculator",
+ "disclaimer": "bewerkbare standaardwaarden — controleer de actuele prijzen",
+ "model": "Model",
+ "expectedOutput": "Verwachte uitvoertokens",
+ "customInput": "Invoer $/MTok",
+ "customOutput": "Uitvoer $/MTok",
+ "inputCost": "Invoerkosten",
+ "outputCost": "Uitvoerkosten",
+ "total": "Totaal",
+ "per1000": "Per 1000 aanroepen"
+ }
+ },
+ "WebhookTester": {
+ "title": "Webhook-tester",
+ "subtitle": "Maak een tijdelijke URL en inspecteer inkomende webhook-verzoeken live.",
+ "intro": {
+ "title": "Maak een webhook-URL om verzoeken te gaan vastleggen",
+ "hint": "Je krijgt een niet te raden URL die elk inkomend verzoek registreert. Deze verloopt na 24 uur."
+ },
+ "createBtn": "Webhook-URL maken",
+ "creating": "Bezig met maken…",
+ "urlLabel": "Jouw webhook-URL",
+ "expiresIn": "Verloopt over {countdown}",
+ "copyUrlBtn": "URL kopiëren",
+ "copied": "Gekopieerd",
+ "clearBtn": "Wissen",
+ "newBinBtn": "Nieuwe URL",
+ "requestsTitle": "Verzoeken",
+ "selectPrompt": "Selecteer een verzoek om de details te inspecteren.",
+ "expiredNotice": "Deze webhook-URL is verlopen. Maak een nieuwe om verder te testen.",
+ "base64Badge": "base64",
+ "truncatedBadge": "afgekapt",
+ "empty": {
+ "title": "Wachten op verzoeken…",
+ "hint": "Stuur een verzoek naar je URL en het verschijnt hier onmiddellijk. Probeer dit:"
+ },
+ "detail": {
+ "receivedAt": "Ontvangen",
+ "source": "Bron-IP",
+ "contentType": "Content-Type",
+ "query": "Query-parameters",
+ "headers": "Headers",
+ "body": "Body",
+ "copyBody": "Body kopiëren",
+ "emptyBody": "Geen body"
+ },
+ "errors": {
+ "createFailed": "Webhook-URL maken mislukt. Probeer het opnieuw.",
+ "clearFailed": "Verzoeken wissen mislukt. Probeer het opnieuw."
+ }
+ },
+ "WebsocketTester": {
+ "title": "WebSocket-tester",
+ "subtitle": "Verbind met een WebSocket-endpoint en verzend, ontvang en inspecteer berichten live",
+ "urlLabel": "Endpoint-URL",
+ "protocolsLabel": "Subprotocollen",
+ "protocolsPlaceholder": "optioneel, gescheiden door komma's",
+ "connect": "Verbinden",
+ "disconnect": "Verbinding verbreken",
+ "status": {
+ "disconnected": "Verbinding verbroken",
+ "connecting": "Verbinden",
+ "connected": "Verbonden",
+ "error": "Fout"
+ },
+ "messages": {
+ "connecting": "Verbinden met {url}…",
+ "connected": "Verbinding tot stand gebracht",
+ "connectedWithProtocol": "Verbinding tot stand gebracht (protocol: {protocol})",
+ "invalidUrl": "Ongeldige URL — moet beginnen met ws:// of wss://",
+ "connectFailed": "Verbinding openen mislukt",
+ "socketError": "Er is een verbindingsfout opgetreden",
+ "notConnected": "Niet verbonden — verbind eerst",
+ "binaryMessage": "Een binair bericht ontvangen"
+ },
+ "log": {
+ "title": "Berichtenlogboek",
+ "empty": "Nog geen berichten. Verbind en begin met verzenden.",
+ "copyAll": "Alles kopiëren",
+ "download": "Downloaden als .txt",
+ "clear": "Logboek wissen",
+ "prettyPrint": "JSON opmaken",
+ "showRaw": "Ruwe tekst tonen"
+ },
+ "composer": {
+ "placeholder": "Typ een bericht om te verzenden…",
+ "send": "Verzenden",
+ "formatJson": "Opmaken als JSON",
+ "invalidJson": "Het bericht is geen geldige JSON",
+ "hint": "Cmd/Ctrl+Enter om te verzenden"
+ },
+ "ping": {
+ "label": "Auto-ping",
+ "seconds": "sec",
+ "intervalTitle": "Ping-interval in seconden",
+ "payloadPlaceholder": "ping-payload"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Whois-opzoeking",
+ "subtitle": "Zoek registratiegegevens van elk domein of IP-adres op via RDAP",
+ "placeholder": "example.com of 8.8.8.8",
+ "lookupBtn": "Opzoeken",
+ "looking": "Bezig met opzoeken…",
+ "refreshBtn": "Vernieuwen",
+ "tryLabel": "Probeer:",
+ "copy": "Kopiëren",
+ "copied": "Gekopieerd",
+ "notAvailable": "Niet beschikbaar",
+ "summary": {
+ "registrar": "Registrar",
+ "network": "Netwerk"
+ },
+ "dates": {
+ "created": "Aangemaakt",
+ "updated": "Bijgewerkt",
+ "expires": "Verloopt"
+ },
+ "sections": {
+ "status": "Status",
+ "nameservers": "Nameservers",
+ "contacts": "Contacten",
+ "rawRdap": "Ruwe RDAP"
+ },
+ "contacts": {
+ "roles": "Rollen",
+ "name": "Naam",
+ "organization": "Organisatie"
+ },
+ "errors": {
+ "invalidTarget": "Voer een geldige domeinnaam of IP-adres in",
+ "lookupFailed": "Opzoeken mislukt (status {status})"
+ }
}
}
diff --git a/apps/web/messages/pl.json b/apps/web/messages/pl.json
index ad8fab76..30418abf 100644
--- a/apps/web/messages/pl.json
+++ b/apps/web/messages/pl.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Kalkulator Chmod",
+ "codeScreenshot": "Zrzut kodu",
+ "cssGenerators": "Generatory CSS",
+ "exifViewer": "Podgląd EXIF",
+ "faviconGenerator": "Generator favicon",
+ "jsonDiff": "Porównanie JSON",
+ "jsonpathPlayground": "Piaskownica JSONPath",
+ "keycodeInspector": "Inspektor klawiszy",
+ "markdownTableGenerator": "Tabela Markdown",
+ "timezoneConverter": "Konwerter stref czasowych",
+ "tokenCounter": "Licznik tokenów",
+ "webhookTester": "Tester webhooków",
+ "websocketTester": "Tester WebSocket",
+ "whoisLookup": "Zapytanie Whois"
},
"Help": {
"title": "Pomoc i dokumentacja",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Generator i weryfikator Bcrypt",
+ "description": "Haszuj hasła za pomocą bcrypt i weryfikuj istniejące hasze."
+ },
+ "chmodCalculator": {
+ "title": "Kalkulator Chmod",
+ "description": "Konwertuj uprawnienia Unix między postacią ósemkową, symboliczną i polami wyboru."
+ },
+ "codeScreenshot": {
+ "title": "Zrzut kodu",
+ "description": "Zamień kod źródłowy w piękne obrazy gotowe do udostępnienia."
+ },
+ "cssGenerators": {
+ "title": "Generatory CSS",
+ "description": "Generatory box-shadow, border-radius i płynnego clamp()."
+ },
+ "exifViewer": {
+ "title": "Podgląd i usuwanie EXIF",
+ "description": "Sprawdzaj i usuwaj metadane EXIF ze zdjęć lokalnie."
+ },
+ "faviconGenerator": {
+ "title": "Generator favicon",
+ "description": "Generuj favicony, ikony aplikacji i fragmenty manifestu."
+ },
+ "jsonDiff": {
+ "title": "Porównanie JSON",
+ "description": "Semantyczne, uwzględniające strukturę porównanie dwóch dokumentów JSON."
+ },
+ "jsonpathPlayground": {
+ "title": "Piaskownica JSONPath",
+ "description": "Odpytuj JSON za pomocą JSONPath lub JMESPath i oglądaj wyniki na żywo."
+ },
+ "keycodeInspector": {
+ "title": "Inspektor klawiszy",
+ "description": "Naciśnij klawisz, aby zobaczyć jego kody zdarzenia JavaScript i modyfikatory."
+ },
+ "markdownTableGenerator": {
+ "title": "Generator tabel Markdown",
+ "description": "Twórz tabele Markdown i HTML w edytorze siatki na wzór arkusza."
+ },
+ "timezoneConverter": {
+ "title": "Konwerter stref czasowych",
+ "description": "Konwertuj czas między wieloma strefami z podpowiedziami o zmianie dnia i godzinach pracy."
+ },
+ "tokenCounter": {
+ "title": "Licznik tokenów LLM",
+ "description": "Licz tokeny LLM, wizualizuj chunki i szacuj koszt API."
+ },
+ "webhookTester": {
+ "title": "Tester webhooków",
+ "description": "Przechwytuj i sprawdzaj przychodzące żądania webhooków w czasie rzeczywistym."
+ },
+ "websocketTester": {
+ "title": "Tester WebSocket",
+ "description": "Łącz się z endpointami WebSocket i wymieniaj wiadomości w czasie rzeczywistym."
+ },
+ "whoisLookup": {
+ "title": "Zapytanie Whois",
+ "description": "Sprawdzaj dane rejestracyjne domen i adresów IP przez RDAP."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Generator i weryfikator Bcrypt",
+ "subtitle": "Haszuj hasła za pomocą bcrypt i weryfikuj hasze — wszystko zostaje w Twojej przeglądarce",
+ "tabs": {
+ "hash": "Hasz",
+ "verify": "Weryfikuj"
+ },
+ "showPassword": "Pokaż hasło",
+ "hidePassword": "Ukryj hasło",
+ "copy": "Kopiuj",
+ "copied": "Skopiowano",
+ "hash": {
+ "passwordLabel": "Hasło",
+ "passwordPlaceholder": "Wpisz hasło do zahaszowania…",
+ "costLabel": "Współczynnik kosztu",
+ "costHint": "Wyższy koszt podwaja pracę na każdym kroku. 10-12 to typowy wybór dla produkcji.",
+ "slowWarning": "Współczynniki kosztu 13 i wyższe mogą obliczać się kilka sekund.",
+ "generate": "Wygeneruj hasz",
+ "generating": "Haszowanie…",
+ "resultLabel": "Hasz bcrypt",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "Budowa hasza",
+ "algorithm": "algorytm",
+ "cost": "koszt",
+ "salt": "sól (22 znaki)",
+ "hash": "hasz (31 znaków)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Hasło",
+ "passwordPlaceholder": "Wpisz hasło do sprawdzenia…",
+ "hashLabel": "Hasz bcrypt",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Wpisz hasło i hasz bcrypt, aby je porównać.",
+ "invalidHash": "To nie wygląda na prawidłowy hasz bcrypt.",
+ "checking": "Porównywanie…",
+ "match": "Zgodne — hasło daje ten hasz.",
+ "noMatch": "Niezgodne — hasło nie daje tego hasza."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Kalkulator Chmod",
+ "subtitle": "Konwertuj uprawnienia plików Unix między polami wyboru, postacią ósemkową i symboliczną",
+ "grid": {
+ "title": "Uprawnienia",
+ "read": "Odczyt",
+ "write": "Zapis",
+ "execute": "Wykonanie",
+ "owner": "Właściciel",
+ "group": "Grupa",
+ "others": "Pozostali"
+ },
+ "special": {
+ "title": "Bity specjalne",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "Ósemkowo",
+ "placeholder": "755 lub 4755",
+ "invalid": "Wpisz 3 lub 4 cyfry ósemkowe (0-7)."
+ },
+ "symbolic": {
+ "label": "Symbolicznie",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Wpisz 9 znaków w rodzaju rwxr-xr-x (dozwolone s/S, t/T)."
+ },
+ "output": {
+ "title": "Polecenie",
+ "copy": "Kopiuj",
+ "copied": "Skopiowano"
+ },
+ "explain": {
+ "line": "{cls} może {perms}.",
+ "none": "{cls} nie ma uprawnień.",
+ "setuid": "Setuid: plik jest uruchamiany z uprawnieniami swojego właściciela.",
+ "setgid": "Setgid: plik jest uruchamiany z uprawnieniami swojej grupy (nowe pliki w katalogu dziedziczą jego grupę).",
+ "sticky": "Sticky: tylko właściciel pliku może go usunąć lub zmienić jego nazwę w tym katalogu."
+ },
+ "presets": {
+ "title": "Popularne ustawienia",
+ "octal": "Ósemkowo",
+ "permissions": "Uprawnienia",
+ "description": "Typowe zastosowanie",
+ "p644": "Zwykłe pliki: właściciel zapisuje, wszyscy odczytują.",
+ "p755": "Pliki wykonywalne i katalogi: wszyscy mogą odczytać i wejść.",
+ "p600": "Pliki prywatne: tylko właściciel odczytuje i zapisuje.",
+ "p700": "Prywatne pliki wykonywalne i katalogi: tylko właściciel.",
+ "p664": "Pliki zapisywalne dla grupy.",
+ "p775": "Katalogi i pliki wykonywalne zapisywalne dla grupy.",
+ "p400": "Tylko do odczytu dla właściciela (np. klucze prywatne SSH)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Zrzut kodu",
+ "subtitle": "Zamień kod w piękne obrazy gotowe do udostępnienia",
+ "languageLabel": "Język",
+ "autoDetect": "Wykrywanie automatyczne",
+ "backgroundLabel": "Tło",
+ "paddingLabel": "Odstęp",
+ "filenameLabel": "Nazwa pliku",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "Kropki okna",
+ "themeLabel": "Motyw okna",
+ "themeDark": "Ciemny",
+ "themeLight": "Jasny",
+ "scaleLabel": "Skala eksportu",
+ "copyImage": "Kopiuj obraz",
+ "copied": "Skopiowano",
+ "exportPng": "Eksportuj PNG",
+ "inputLabel": "Kod",
+ "detected": "Wykryto: {language}",
+ "clear": "Wyczyść",
+ "placeholder": "Wklej swój kod tutaj…",
+ "previewLabel": "Podgląd",
+ "emptyPreview": "Podgląd Twojego kodu pojawi się tutaj",
+ "exportFailed": "Nie udało się wyeksportować obrazu",
+ "copyUnsupported": "Kopiowanie obrazów nie jest obsługiwane w tej przeglądarce",
+ "copyFailed": "Nie udało się skopiować obrazu",
+ "backgrounds": {
+ "ocean": "Ocean",
+ "sunset": "Zachód słońca",
+ "candy": "Cukierek",
+ "forest": "Las",
+ "midnight": "Północ",
+ "gold": "Złoto",
+ "aurora": "Zorza",
+ "slate": "Łupek",
+ "transparent": "Przezroczysty"
+ }
+ },
+ "CssGenerators": {
+ "title": "Generatory CSS",
+ "subtitle": "Box-shadow, border-radius i płynny clamp() — skopiuj CSS.",
+ "preview": "Podgląd",
+ "output": "CSS",
+ "copy": "Kopiuj",
+ "copied": "Skopiowano",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "Płynny clamp()"
+ },
+ "boxShadow": {
+ "presets": "Ustawienia",
+ "layer": "Warstwa",
+ "addLayer": "Dodaj warstwę",
+ "removeLayer": "Usuń warstwę",
+ "x": "X",
+ "y": "Y",
+ "blur": "Rozmycie",
+ "spread": "Rozszerzenie",
+ "color": "Kolor",
+ "alpha": "Alfa",
+ "inset": "Wewnętrzny",
+ "presetNames": {
+ "subtleCard": "Subtelna karta",
+ "elevated": "Uniesiony",
+ "hardOffset": "Twarde przesunięcie",
+ "neonGlow": "Neonowa poświata",
+ "inner": "Wewnętrzny",
+ "layeredSmooth": "Miękkie warstwy"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Tryb",
+ "modes": {
+ "simple": "Na róg",
+ "blob": "Organiczna plama"
+ },
+ "unit": "Jednostka",
+ "linkCorners": "Powiąż rogi",
+ "randomize": "Losuj",
+ "horizontal": "Promienie poziome",
+ "vertical": "Promienie pionowe",
+ "corners": {
+ "tl": "Górny lewy",
+ "tr": "Górny prawy",
+ "br": "Dolny prawy",
+ "bl": "Dolny lewy"
+ }
+ },
+ "clamp": {
+ "minFont": "Min. czcionka (px)",
+ "maxFont": "Maks. czcionka (px)",
+ "minViewport": "Min. viewport (px)",
+ "maxViewport": "Maks. viewport (px)",
+ "rootFont": "Czcionka główna (px)",
+ "viewportWidth": "Symulowany viewport",
+ "computedSize": "Obliczony rozmiar",
+ "slope": "Nachylenie",
+ "intercept": "Punkt przecięcia",
+ "previewText": "Szybki brązowy lis",
+ "invalid": "Wpisz prawidłowe i różniące się granice viewportu."
+ }
+ },
+ "ExifViewer": {
+ "title": "Podgląd i usuwanie EXIF",
+ "subtitle": "Sprawdzaj metadane zdjęć i usuwaj je w swojej przeglądarce",
+ "dropTitle": "Upuść obraz tutaj",
+ "dropHint": "lub kliknij, aby przeglądać — Twój obraz nigdy nie opuszcza urządzenia",
+ "browse": "Wybierz obraz",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Odczytywanie metadanych…",
+ "parseFailed": "Nie udało się odczytać tego obrazu",
+ "clear": "Usuń obraz",
+ "gpsWarning": "Zawiera lokalizację GPS",
+ "openMap": "Zobacz na OpenStreetMap",
+ "noMetadata": "Nie znaleziono osadzonych metadanych w tym obrazie.",
+ "facts": {
+ "name": "Nazwa",
+ "type": "Typ",
+ "dimensions": "Wymiary",
+ "size": "Rozmiar"
+ },
+ "groups": {
+ "camera": "Aparat",
+ "exposure": "Ekspozycja",
+ "timestamps": "Znaczniki czasu",
+ "gps": "Lokalizacja GPS",
+ "software": "Oprogramowanie i inne"
+ },
+ "gpsCard": {
+ "latitude": "Szerokość geogr.",
+ "longitude": "Długość geogr.",
+ "altitude": "Wysokość"
+ },
+ "raw": {
+ "title": "Wszystkie metadane",
+ "copyJson": "Kopiuj JSON",
+ "copied": "Skopiowano"
+ },
+ "remove": {
+ "title": "Usuń metadane",
+ "jpegDescription": "Bezstratnie usuń EXIF, XMP, IPTC i komentarze bez ponownej kompresji obrazu.",
+ "reencodeDescription": "Przekoduj obraz, aby usunąć osadzone metadane. Powoduje to ponowną kompresję pikseli.",
+ "keepIcc": "Zachowaj profil kolorów (ICC)",
+ "stripButton": "Usuń metadane",
+ "reencodeButton": "Przekoduj i usuń",
+ "unsupported": "Usuwanie metadanych nie jest obsługiwane dla tego typu pliku.",
+ "failed": "Nie udało się usunąć metadanych",
+ "before": "Przed",
+ "after": "Po",
+ "download": "Pobierz oczyszczony obraz"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Generator favicon",
+ "subtitle": "Twórz favicony i ikony aplikacji z jednego obrazu",
+ "dropTitle": "Upuść obraz tutaj",
+ "dropHint": "PNG, JPEG lub SVG — kliknij, aby przeglądać",
+ "browse": "Wybierz obraz",
+ "clear": "Usuń obraz",
+ "backgroundLabel": "Tło",
+ "transparent": "Przezroczysty",
+ "solidColor": "Jednolity",
+ "cornerLabel": "Kształt rogów",
+ "cornerNone": "Kwadrat",
+ "cornerRounded": "Zaokrąglony",
+ "cornerCircle": "Koło",
+ "maskPaddingLabel": "Odstęp maskable",
+ "maskable": "maskable",
+ "previewLabel": "Podgląd",
+ "copy": "Kopiuj",
+ "headSnippet": "Fragment head HTML",
+ "downloadIco": "Pobierz favicon.ico",
+ "downloadZip": "Pobierz wszystko (.zip)"
+ },
+ "JsonDiff": {
+ "title": "Porównanie JSON",
+ "subtitle": "Semantyczne, uwzględniające strukturę porównanie dwóch dokumentów JSON — kolejność kluczy i formatowanie są ignorowane",
+ "options": {
+ "ignoreArrayOrder": "Ignoruj kolejność tablic"
+ },
+ "loadBothSamples": "Wczytaj przykłady",
+ "sample": "Przykład",
+ "format": "Formatuj",
+ "clear": "Wyczyść",
+ "formatError": "Nie można sformatować: dane wejściowe nie są prawidłowym JSON",
+ "placeholder": "Wklej lub wpisz JSON tutaj…",
+ "panels": {
+ "left": "Oryginał (lewa)",
+ "right": "Zmodyfikowany (prawa)",
+ "results": "Różnice"
+ },
+ "summary": {
+ "added": "{count} dodano",
+ "removed": "{count} usunięto",
+ "changed": "{count} zmieniono"
+ },
+ "types": {
+ "added": "Dodano",
+ "removed": "Usunięto",
+ "changed": "Zmieniono"
+ },
+ "copyChanges": "Kopiuj jako JSON",
+ "copied": "Skopiowano",
+ "empty": "Wklej JSON do obu paneli, aby zobaczyć ich różnice.",
+ "identical": "Oba dokumenty są semantycznie identyczne."
+ },
+ "JsonpathPlayground": {
+ "title": "Piaskownica JSONPath",
+ "subtitle": "Odpytuj dokumenty JSON za pomocą JSONPath lub JMESPath i oglądaj wyniki na żywo",
+ "engineLabel": "Silnik",
+ "queryLabel": "Zapytanie",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Ściąga",
+ "cheats": {
+ "jpAllTitles": "Wszystkie tytuły książek",
+ "jpAllAuthors": "Wszyscy autorzy, w dowolnym miejscu dokumentu",
+ "jpFirstBook": "Pierwsza książka",
+ "jpLastBook": "Ostatnia książka",
+ "jpSlice": "Pierwsze dwie książki (wycinek)",
+ "jpFilterPrice": "Książki tańsze niż 40",
+ "jpFilterInStock": "Tytuły książek dostępnych w magazynie",
+ "jpRecursive": "Wszystkie wartości, rekurencyjnie",
+ "jmAllTitles": "Wszystkie tytuły książek",
+ "jmFirstAuthor": "Autor pierwszej książki",
+ "jmLastBook": "Ostatnia książka",
+ "jmSlice": "Pierwsze dwie książki (wycinek)",
+ "jmFilterPrice": "Tytuły książek tańszych niż 40",
+ "jmCount": "Policz książki dostępne w magazynie",
+ "jmProjection": "Przekształć każdą książkę w nowy obiekt",
+ "jmSort": "Tytuły posortowane według ceny"
+ },
+ "panels": {
+ "input": "Wejście JSON",
+ "results": "Wyniki"
+ },
+ "sample": "Przykład",
+ "format": "Formatuj",
+ "clear": "Wyczyść",
+ "formatError": "Nie można sformatować: dane wejściowe nie są prawidłowym JSON",
+ "placeholder": "Wklej lub wpisz JSON tutaj…",
+ "outputPlaceholder": "Dopasowania pojawią się tutaj…",
+ "matchCount": "{count, plural, one {# dopasowanie} few {# dopasowania} many {# dopasowań} other {# dopasowania}}",
+ "copy": "Kopiuj",
+ "copied": "Skopiowano"
+ },
+ "KeycodeInspector": {
+ "title": "Inspektor klawiszy",
+ "subtitle": "Naciśnij dowolny klawisz, aby zbadać jego zdarzenie klawiatury JavaScript",
+ "capture": {
+ "prompt": "Naciśnij dowolny klawisz…",
+ "ready": "Nasłuchiwanie — naciśnij dowolny klawisz na klawiaturze",
+ "clickToFocus": "Kliknij tutaj, aby ustawić fokus, a następnie naciśnij dowolny klawisz"
+ },
+ "hero": {
+ "spaceKey": "(Spacja)",
+ "keyLabel": "event.key",
+ "repeat": "powtórzenie"
+ },
+ "copyJson": "Kopiuj JSON",
+ "copiedJson": "JSON zdarzenia skopiowano do schowka",
+ "deprecated": "przestarzałe",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Standardowa",
+ "left": "Lewa",
+ "right": "Prawa",
+ "numpad": "Klawiatura numeryczna",
+ "unknown": "Nieznana"
+ },
+ "history": {
+ "title": "Historia",
+ "clear": "Wyczyść historię",
+ "empty": "Naciśnięcia klawiszy pojawią się tutaj."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Generator tabel Markdown",
+ "subtitle": "Edytuj siatkę, wklej z arkusza i skopiuj ładnie wyrównaną tabelę Markdown lub HTML",
+ "addRow": "Dodaj wiersz",
+ "addColumn": "Dodaj kolumnę",
+ "importLabel": "Importuj",
+ "importPlaceholder": "Wklej TSV (z Excela / Arkuszy Google) lub CSV tutaj…",
+ "importButton": "Importuj do siatki",
+ "pasteClipboard": "Wklej ze schowka",
+ "importEmpty": "Nie ma czego importować — najpierw wklej kilka wierszy",
+ "clipboardError": "Nie udało się odczytać ze schowka",
+ "clear": "Wyczyść",
+ "panels": {
+ "editor": "Edytor tabeli",
+ "output": "Wynik"
+ },
+ "align": {
+ "left": "Wyrównaj do lewej",
+ "center": "Wyśrodkuj",
+ "right": "Wyrównaj do prawej"
+ },
+ "headerPlaceholder": "Nagłówek",
+ "cellPlaceholder": "Komórka",
+ "removeRow": "Usuń wiersz",
+ "removeColumn": "Usuń kolumnę",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Kopiuj",
+ "copied": "Skopiowano"
+ },
+ "TimezoneConverter": {
+ "title": "Konwerter stref czasowych",
+ "subtitle": "Konwertuj jeden moment na całym świecie, aby planować spotkania.",
+ "dateTimeLabel": "Data i godzina",
+ "now": "Teraz",
+ "sourceZoneLabel": "Źródłowa strefa czasowa",
+ "selectZone": "Wybierz strefę",
+ "searchZone": "Szukaj stref czasowych...",
+ "noZone": "Nie znaleziono strefy czasowej.",
+ "scrubLabel": "Przesuń czas",
+ "reset": "Resetuj",
+ "targetsLabel": "Strefy docelowe",
+ "addZone": "Dodaj strefę",
+ "remove": "Usuń",
+ "localBadge": "Lokalna",
+ "invalidDate": "Wpisz prawidłową datę i godzinę.",
+ "bands": {
+ "work": "Godziny pracy (09:00–18:00)",
+ "edge": "Godziny brzegowe (wczesny ranek / wieczór)",
+ "night": "Noc"
+ }
+ },
+ "TokenCounter": {
+ "title": "Licznik tokenów LLM",
+ "subtitle": "Licz tokeny, wizualizuj chunki i szacuj koszt API.",
+ "inputLabel": "Tekst",
+ "sample": "Przykład",
+ "clear": "Wyczyść",
+ "placeholder": "Wklej tekst, kod lub JSON, aby policzyć tokeny...",
+ "encoding": "Kodowanie",
+ "loading": "Ładowanie tokenizera...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / seria o)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Tokeny",
+ "characters": "Znaki",
+ "words": "Słowa",
+ "ratio": "Tokeny/słowo",
+ "ratioSub": "ok."
+ },
+ "claudeRow": "Claude (przybliżone)",
+ "estimate": "szacunek",
+ "viz": {
+ "title": "Wizualizacja tokenów",
+ "toggle": "Pokaż",
+ "hint": "Włącz, aby pokolorować każdy token jako chunk.",
+ "empty": "Wpisz tekst, aby zwizualizować tokeny.",
+ "truncated": "Pokazano pierwsze {limit} z {total} tokenów."
+ },
+ "cost": {
+ "title": "Kalkulator kosztów",
+ "disclaimer": "edytowalne wartości domyślne — sprawdź aktualne ceny",
+ "model": "Model",
+ "expectedOutput": "Oczekiwane tokeny wyjściowe",
+ "customInput": "Wejście $/MTok",
+ "customOutput": "Wyjście $/MTok",
+ "inputCost": "Koszt wejścia",
+ "outputCost": "Koszt wyjścia",
+ "total": "Razem",
+ "per1000": "Na 1000 wywołań"
+ }
+ },
+ "WebhookTester": {
+ "title": "Tester webhooków",
+ "subtitle": "Utwórz tymczasowy URL i sprawdzaj przychodzące żądania webhooków na żywo.",
+ "intro": {
+ "title": "Utwórz URL webhooka, aby zacząć przechwytywać żądania",
+ "hint": "Otrzymasz niemożliwy do odgadnięcia URL, który rejestruje każde przychodzące żądanie. Wygasa po 24 godzinach."
+ },
+ "createBtn": "Utwórz URL webhooka",
+ "creating": "Tworzenie…",
+ "urlLabel": "Twój URL webhooka",
+ "expiresIn": "Wygasa za {countdown}",
+ "copyUrlBtn": "Kopiuj URL",
+ "copied": "Skopiowano",
+ "clearBtn": "Wyczyść",
+ "newBinBtn": "Nowy URL",
+ "requestsTitle": "Żądania",
+ "selectPrompt": "Wybierz żądanie, aby sprawdzić jego szczegóły.",
+ "expiredNotice": "Ten URL webhooka wygasł. Utwórz nowy, aby kontynuować testowanie.",
+ "base64Badge": "base64",
+ "truncatedBadge": "obcięte",
+ "empty": {
+ "title": "Oczekiwanie na żądania…",
+ "hint": "Wyślij żądanie na swój URL, a pojawi się tutaj natychmiast. Spróbuj tak:"
+ },
+ "detail": {
+ "receivedAt": "Odebrano",
+ "source": "IP źródłowe",
+ "contentType": "Content-Type",
+ "query": "Parametry query",
+ "headers": "Nagłówki",
+ "body": "Treść",
+ "copyBody": "Kopiuj treść",
+ "emptyBody": "Brak treści"
+ },
+ "errors": {
+ "createFailed": "Nie udało się utworzyć URL webhooka. Spróbuj ponownie.",
+ "clearFailed": "Nie udało się wyczyścić żądań. Spróbuj ponownie."
+ }
+ },
+ "WebsocketTester": {
+ "title": "Tester WebSocket",
+ "subtitle": "Połącz się z endpointem WebSocket i wysyłaj, odbieraj oraz sprawdzaj wiadomości na żywo",
+ "urlLabel": "URL endpointu",
+ "protocolsLabel": "Podprotokoły",
+ "protocolsPlaceholder": "opcjonalnie, oddzielone przecinkami",
+ "connect": "Połącz",
+ "disconnect": "Rozłącz",
+ "status": {
+ "disconnected": "Rozłączono",
+ "connecting": "Łączenie",
+ "connected": "Połączono",
+ "error": "Błąd"
+ },
+ "messages": {
+ "connecting": "Łączenie z {url}…",
+ "connected": "Połączenie nawiązane",
+ "connectedWithProtocol": "Połączenie nawiązane (protokół: {protocol})",
+ "invalidUrl": "Nieprawidłowy URL — musi zaczynać się od ws:// lub wss://",
+ "connectFailed": "Nie udało się otworzyć połączenia",
+ "socketError": "Wystąpił błąd połączenia",
+ "notConnected": "Nie połączono — najpierw się połącz",
+ "binaryMessage": "Odebrano wiadomość binarną"
+ },
+ "log": {
+ "title": "Dziennik wiadomości",
+ "empty": "Brak wiadomości. Połącz się i zacznij wysyłać.",
+ "copyAll": "Kopiuj wszystko",
+ "download": "Pobierz jako .txt",
+ "clear": "Wyczyść dziennik",
+ "prettyPrint": "Formatuj JSON",
+ "showRaw": "Pokaż surowy tekst"
+ },
+ "composer": {
+ "placeholder": "Wpisz wiadomość do wysłania…",
+ "send": "Wyślij",
+ "formatJson": "Formatuj jako JSON",
+ "invalidJson": "Wiadomość nie jest prawidłowym JSON",
+ "hint": "Cmd/Ctrl+Enter, aby wysłać"
+ },
+ "ping": {
+ "label": "Auto-ping",
+ "seconds": "s",
+ "intervalTitle": "Interwał pingu w sekundach",
+ "payloadPlaceholder": "payload pingu"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Zapytanie Whois",
+ "subtitle": "Sprawdzaj dane rejestracyjne dowolnej domeny lub adresu IP przez RDAP",
+ "placeholder": "example.com lub 8.8.8.8",
+ "lookupBtn": "Sprawdź",
+ "looking": "Sprawdzanie…",
+ "refreshBtn": "Odśwież",
+ "tryLabel": "Spróbuj:",
+ "copy": "Kopiuj",
+ "copied": "Skopiowano",
+ "notAvailable": "Niedostępne",
+ "summary": {
+ "registrar": "Rejestrator",
+ "network": "Sieć"
+ },
+ "dates": {
+ "created": "Utworzono",
+ "updated": "Zaktualizowano",
+ "expires": "Wygasa"
+ },
+ "sections": {
+ "status": "Status",
+ "nameservers": "Serwery nazw",
+ "contacts": "Kontakty",
+ "rawRdap": "Surowy RDAP"
+ },
+ "contacts": {
+ "roles": "Role",
+ "name": "Nazwa",
+ "organization": "Organizacja"
+ },
+ "errors": {
+ "invalidTarget": "Wpisz prawidłową nazwę domeny lub adres IP",
+ "lookupFailed": "Zapytanie nie powiodło się (status {status})"
+ }
}
}
diff --git a/apps/web/messages/pt-BR.json b/apps/web/messages/pt-BR.json
index 5cf620ae..a5171541 100644
--- a/apps/web/messages/pt-BR.json
+++ b/apps/web/messages/pt-BR.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Calculadora Chmod",
+ "codeScreenshot": "Captura de Código",
+ "cssGenerators": "Geradores CSS",
+ "exifViewer": "Visualizador EXIF",
+ "faviconGenerator": "Gerador de Favicon",
+ "jsonDiff": "Diff JSON",
+ "jsonpathPlayground": "Playground JSONPath",
+ "keycodeInspector": "Inspetor de Keycode",
+ "markdownTableGenerator": "Tabela Markdown",
+ "timezoneConverter": "Conversor de Fuso Horário",
+ "tokenCounter": "Contador de Tokens",
+ "webhookTester": "Testador de Webhook",
+ "websocketTester": "Testador de WebSocket",
+ "whoisLookup": "Consulta Whois"
},
"Help": {
"title": "Ajuda e documentação",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Gerador e Verificador Bcrypt",
+ "description": "Gere hash de senhas com bcrypt e verifique hashes existentes."
+ },
+ "chmodCalculator": {
+ "title": "Calculadora Chmod",
+ "description": "Converta permissões Unix entre octal, simbólico e caixas de seleção."
+ },
+ "codeScreenshot": {
+ "title": "Captura de Código",
+ "description": "Transforme código-fonte em belas imagens compartilháveis."
+ },
+ "cssGenerators": {
+ "title": "Geradores CSS",
+ "description": "Geradores de box-shadow, border-radius e clamp() fluido."
+ },
+ "exifViewer": {
+ "title": "Visualizador e Removedor EXIF",
+ "description": "Inspecione e remova metadados EXIF de fotos localmente."
+ },
+ "faviconGenerator": {
+ "title": "Gerador de Favicon",
+ "description": "Gere favicons, ícones de app e trechos de manifest."
+ },
+ "jsonDiff": {
+ "title": "Diff JSON",
+ "description": "Diff semântico e ciente de estrutura de dois documentos JSON."
+ },
+ "jsonpathPlayground": {
+ "title": "Playground JSONPath",
+ "description": "Consulte JSON com JSONPath ou JMESPath e veja resultados ao vivo."
+ },
+ "keycodeInspector": {
+ "title": "Inspetor de Keycode",
+ "description": "Pressione uma tecla para ver seus códigos de evento JavaScript e modificadores."
+ },
+ "markdownTableGenerator": {
+ "title": "Gerador de Tabela Markdown",
+ "description": "Crie tabelas Markdown e HTML em um editor de grade tipo planilha."
+ },
+ "timezoneConverter": {
+ "title": "Conversor de Fuso Horário",
+ "description": "Converta um horário entre vários fusos com indicações de mudança de dia e horário comercial."
+ },
+ "tokenCounter": {
+ "title": "Contador de Tokens LLM",
+ "description": "Conte tokens de LLM, visualize chunks e estime o custo de API."
+ },
+ "webhookTester": {
+ "title": "Testador de Webhook",
+ "description": "Capture e inspecione requisições de webhook recebidas em tempo real."
+ },
+ "websocketTester": {
+ "title": "Testador de WebSocket",
+ "description": "Conecte-se a endpoints WebSocket e troque mensagens em tempo real."
+ },
+ "whoisLookup": {
+ "title": "Consulta Whois",
+ "description": "Consulte detalhes de registro de domínio e IP via RDAP."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Gerador e Verificador Bcrypt",
+ "subtitle": "Gere hash de senhas com bcrypt e verifique hashes — tudo permanece no seu navegador",
+ "tabs": {
+ "hash": "Hash",
+ "verify": "Verificar"
+ },
+ "showPassword": "Mostrar senha",
+ "hidePassword": "Ocultar senha",
+ "copy": "Copiar",
+ "copied": "Copiado",
+ "hash": {
+ "passwordLabel": "Senha",
+ "passwordPlaceholder": "Digite uma senha para gerar o hash…",
+ "costLabel": "Fator de custo",
+ "costHint": "Um custo maior dobra o trabalho por etapa. 10-12 é uma escolha comum para produção.",
+ "slowWarning": "Fatores de custo de 13 ou mais podem levar vários segundos para calcular.",
+ "generate": "Gerar hash",
+ "generating": "Gerando hash…",
+ "resultLabel": "Hash bcrypt",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "Anatomia do hash",
+ "algorithm": "algoritmo",
+ "cost": "custo",
+ "salt": "salt (22 caracteres)",
+ "hash": "hash (31 caracteres)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Senha",
+ "passwordPlaceholder": "Digite a senha para verificar…",
+ "hashLabel": "Hash bcrypt",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Digite uma senha e um hash bcrypt para compará-los.",
+ "invalidHash": "Isto não parece um hash bcrypt válido.",
+ "checking": "Comparando…",
+ "match": "Corresponde — a senha produz este hash.",
+ "noMatch": "Não corresponde — a senha não produz este hash."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Calculadora Chmod",
+ "subtitle": "Converta permissões de arquivo Unix entre caixas de seleção, octal e notação simbólica",
+ "grid": {
+ "title": "Permissões",
+ "read": "Leitura",
+ "write": "Escrita",
+ "execute": "Execução",
+ "owner": "Dono",
+ "group": "Grupo",
+ "others": "Outros"
+ },
+ "special": {
+ "title": "Bits especiais",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "Octal",
+ "placeholder": "755 ou 4755",
+ "invalid": "Digite 3 ou 4 dígitos octais (0-7)."
+ },
+ "symbolic": {
+ "label": "Simbólico",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Digite 9 caracteres como rwxr-xr-x (s/S, t/T permitidos)."
+ },
+ "output": {
+ "title": "Comando",
+ "copy": "Copiar",
+ "copied": "Copiado"
+ },
+ "explain": {
+ "line": "{cls} pode {perms}.",
+ "none": "{cls} não tem permissões.",
+ "setuid": "Setuid: o arquivo é executado com os privilégios de seu dono.",
+ "setgid": "Setgid: o arquivo é executado com os privilégios de seu grupo (novos arquivos em um diretório herdam seu grupo).",
+ "sticky": "Sticky: apenas o dono de um arquivo pode excluí-lo ou renomeá-lo dentro deste diretório."
+ },
+ "presets": {
+ "title": "Presets comuns",
+ "octal": "Octal",
+ "permissions": "Permissões",
+ "description": "Uso típico",
+ "p644": "Arquivos comuns: o dono escreve, todos leem.",
+ "p755": "Executáveis e diretórios: todos podem ler e entrar.",
+ "p600": "Arquivos privados: apenas o dono lê e escreve.",
+ "p700": "Executáveis e diretórios privados: apenas o dono.",
+ "p664": "Arquivos graváveis pelo grupo.",
+ "p775": "Diretórios e executáveis graváveis pelo grupo.",
+ "p400": "Somente leitura para o dono (ex.: chaves privadas SSH)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Captura de Código",
+ "subtitle": "Transforme código em belas imagens compartilháveis",
+ "languageLabel": "Linguagem",
+ "autoDetect": "Detecção automática",
+ "backgroundLabel": "Plano de fundo",
+ "paddingLabel": "Espaçamento",
+ "filenameLabel": "Nome do arquivo",
+ "filenamePlaceholder": "exemplo.ts",
+ "windowLabel": "Pontos da janela",
+ "themeLabel": "Tema da janela",
+ "themeDark": "Escuro",
+ "themeLight": "Claro",
+ "scaleLabel": "Escala de exportação",
+ "copyImage": "Copiar imagem",
+ "copied": "Copiado",
+ "exportPng": "Exportar PNG",
+ "inputLabel": "Código",
+ "detected": "Detectado: {language}",
+ "clear": "Limpar",
+ "placeholder": "Cole seu código aqui…",
+ "previewLabel": "Pré-visualização",
+ "emptyPreview": "A pré-visualização do seu código aparecerá aqui",
+ "exportFailed": "Falha ao exportar a imagem",
+ "copyUnsupported": "A cópia de imagens não é suportada neste navegador",
+ "copyFailed": "Falha ao copiar a imagem",
+ "backgrounds": {
+ "ocean": "Oceano",
+ "sunset": "Pôr do sol",
+ "candy": "Doce",
+ "forest": "Floresta",
+ "midnight": "Meia-noite",
+ "gold": "Ouro",
+ "aurora": "Aurora",
+ "slate": "Ardósia",
+ "transparent": "Transparente"
+ }
+ },
+ "CssGenerators": {
+ "title": "Geradores CSS",
+ "subtitle": "Box-shadow, border-radius e clamp() fluido — copie o CSS.",
+ "preview": "Pré-visualização",
+ "output": "CSS",
+ "copy": "Copiar",
+ "copied": "Copiado",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "clamp() fluido"
+ },
+ "boxShadow": {
+ "presets": "Presets",
+ "layer": "Camada",
+ "addLayer": "Adicionar camada",
+ "removeLayer": "Remover camada",
+ "x": "X",
+ "y": "Y",
+ "blur": "Desfoque",
+ "spread": "Propagação",
+ "color": "Cor",
+ "alpha": "Alfa",
+ "inset": "Interno",
+ "presetNames": {
+ "subtleCard": "Cartão sutil",
+ "elevated": "Elevado",
+ "hardOffset": "Deslocamento rígido",
+ "neonGlow": "Brilho neon",
+ "inner": "Interno",
+ "layeredSmooth": "Camadas suaves"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Modo",
+ "modes": {
+ "simple": "Por canto",
+ "blob": "Blob orgânico"
+ },
+ "unit": "Unidade",
+ "linkCorners": "Vincular cantos",
+ "randomize": "Aleatorizar",
+ "horizontal": "Raios horizontais",
+ "vertical": "Raios verticais",
+ "corners": {
+ "tl": "Superior esquerdo",
+ "tr": "Superior direito",
+ "br": "Inferior direito",
+ "bl": "Inferior esquerdo"
+ }
+ },
+ "clamp": {
+ "minFont": "Fonte mínima (px)",
+ "maxFont": "Fonte máxima (px)",
+ "minViewport": "Viewport mínima (px)",
+ "maxViewport": "Viewport máxima (px)",
+ "rootFont": "Fonte raiz (px)",
+ "viewportWidth": "Viewport simulada",
+ "computedSize": "Tamanho calculado",
+ "slope": "Inclinação",
+ "intercept": "Intercepto",
+ "previewText": "A rápida raposa marrom",
+ "invalid": "Digite limites de viewport válidos e diferentes."
+ }
+ },
+ "ExifViewer": {
+ "title": "Visualizador e Removedor EXIF",
+ "subtitle": "Inspecione os metadados de fotos e remova-os no seu navegador",
+ "dropTitle": "Solte uma imagem aqui",
+ "dropHint": "ou clique para procurar — sua imagem nunca sai do seu dispositivo",
+ "browse": "Escolher imagem",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Lendo metadados…",
+ "parseFailed": "Falha ao ler esta imagem",
+ "clear": "Remover imagem",
+ "gpsWarning": "Contém localização GPS",
+ "openMap": "Ver no OpenStreetMap",
+ "noMetadata": "Nenhum metadado incorporado foi encontrado nesta imagem.",
+ "facts": {
+ "name": "Nome",
+ "type": "Tipo",
+ "dimensions": "Dimensões",
+ "size": "Tamanho"
+ },
+ "groups": {
+ "camera": "Câmera",
+ "exposure": "Exposição",
+ "timestamps": "Carimbos de data/hora",
+ "gps": "Localização GPS",
+ "software": "Software e outros"
+ },
+ "gpsCard": {
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "altitude": "Altitude"
+ },
+ "raw": {
+ "title": "Todos os metadados",
+ "copyJson": "Copiar JSON",
+ "copied": "Copiado"
+ },
+ "remove": {
+ "title": "Remover metadados",
+ "jpegDescription": "Remova EXIF, XMP, IPTC e comentários sem perdas, sem recomprimir a imagem.",
+ "reencodeDescription": "Recodifique a imagem para remover metadados incorporados. Isso recomprime os pixels.",
+ "keepIcc": "Manter perfil de cor (ICC)",
+ "stripButton": "Remover metadados",
+ "reencodeButton": "Recodificar e remover",
+ "unsupported": "A remoção de metadados não é suportada para este tipo de arquivo.",
+ "failed": "Falha ao remover metadados",
+ "before": "Antes",
+ "after": "Depois",
+ "download": "Baixar imagem limpa"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Gerador de Favicon",
+ "subtitle": "Crie favicons e ícones de app a partir de uma imagem",
+ "dropTitle": "Solte uma imagem aqui",
+ "dropHint": "PNG, JPEG ou SVG — clique para procurar",
+ "browse": "Escolher imagem",
+ "clear": "Remover imagem",
+ "backgroundLabel": "Plano de fundo",
+ "transparent": "Transparente",
+ "solidColor": "Sólido",
+ "cornerLabel": "Formato do canto",
+ "cornerNone": "Quadrado",
+ "cornerRounded": "Arredondado",
+ "cornerCircle": "Círculo",
+ "maskPaddingLabel": "Espaçamento maskable",
+ "maskable": "maskable",
+ "previewLabel": "Pré-visualização",
+ "copy": "Copiar",
+ "headSnippet": "Trecho do head HTML",
+ "downloadIco": "Baixar favicon.ico",
+ "downloadZip": "Baixar tudo (.zip)"
+ },
+ "JsonDiff": {
+ "title": "Diff JSON",
+ "subtitle": "Comparação semântica e ciente de estrutura de dois documentos JSON — ordem de chaves e formatação ignoradas",
+ "options": {
+ "ignoreArrayOrder": "Ignorar ordem dos arrays"
+ },
+ "loadBothSamples": "Carregar exemplos",
+ "sample": "Exemplo",
+ "format": "Formatar",
+ "clear": "Limpar",
+ "formatError": "Não é possível formatar: a entrada não é um JSON válido",
+ "placeholder": "Cole ou digite JSON aqui…",
+ "panels": {
+ "left": "Original (esquerda)",
+ "right": "Modificado (direita)",
+ "results": "Diferenças"
+ },
+ "summary": {
+ "added": "{count} adicionado(s)",
+ "removed": "{count} removido(s)",
+ "changed": "{count} alterado(s)"
+ },
+ "types": {
+ "added": "Adicionado",
+ "removed": "Removido",
+ "changed": "Alterado"
+ },
+ "copyChanges": "Copiar como JSON",
+ "copied": "Copiado",
+ "empty": "Cole JSON nos dois painéis para ver suas diferenças.",
+ "identical": "Os dois documentos são semanticamente idênticos."
+ },
+ "JsonpathPlayground": {
+ "title": "Playground JSONPath",
+ "subtitle": "Consulte documentos JSON com JSONPath ou JMESPath e veja resultados ao vivo",
+ "engineLabel": "Motor",
+ "queryLabel": "Consulta",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Referência rápida",
+ "cheats": {
+ "jpAllTitles": "Todos os títulos de livros",
+ "jpAllAuthors": "Todos os autores, em qualquer lugar do documento",
+ "jpFirstBook": "Primeiro livro",
+ "jpLastBook": "Último livro",
+ "jpSlice": "Primeiros dois livros (slice)",
+ "jpFilterPrice": "Livros mais baratos que 40",
+ "jpFilterInStock": "Títulos de livros em estoque",
+ "jpRecursive": "Todos os valores, recursivamente",
+ "jmAllTitles": "Todos os títulos de livros",
+ "jmFirstAuthor": "Autor do primeiro livro",
+ "jmLastBook": "Último livro",
+ "jmSlice": "Primeiros dois livros (slice)",
+ "jmFilterPrice": "Títulos de livros mais baratos que 40",
+ "jmCount": "Contar livros em estoque",
+ "jmProjection": "Remodelar cada livro em um novo objeto",
+ "jmSort": "Títulos ordenados por preço"
+ },
+ "panels": {
+ "input": "Entrada JSON",
+ "results": "Resultados"
+ },
+ "sample": "Exemplo",
+ "format": "Formatar",
+ "clear": "Limpar",
+ "formatError": "Não é possível formatar: a entrada não é um JSON válido",
+ "placeholder": "Cole ou digite JSON aqui…",
+ "outputPlaceholder": "As correspondências aparecerão aqui…",
+ "matchCount": "{count, plural, one {# correspondência} other {# correspondências}}",
+ "copy": "Copiar",
+ "copied": "Copiado"
+ },
+ "KeycodeInspector": {
+ "title": "Inspetor de Keycode",
+ "subtitle": "Pressione qualquer tecla para inspecionar seu evento de teclado JavaScript",
+ "capture": {
+ "prompt": "Pressione qualquer tecla…",
+ "ready": "Ouvindo — pressione qualquer tecla no seu teclado",
+ "clickToFocus": "Clique aqui para focar e depois pressione qualquer tecla"
+ },
+ "hero": {
+ "spaceKey": "(Espaço)",
+ "keyLabel": "event.key",
+ "repeat": "repetição"
+ },
+ "copyJson": "Copiar JSON",
+ "copiedJson": "JSON do evento copiado para a área de transferência",
+ "deprecated": "obsoleto",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Padrão",
+ "left": "Esquerda",
+ "right": "Direita",
+ "numpad": "Teclado numérico",
+ "unknown": "Desconhecido"
+ },
+ "history": {
+ "title": "Histórico",
+ "clear": "Limpar histórico",
+ "empty": "As teclas pressionadas aparecerão aqui."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Gerador de Tabela Markdown",
+ "subtitle": "Edite uma grade, cole de uma planilha e copie uma tabela Markdown ou HTML bem alinhada",
+ "addRow": "Adicionar linha",
+ "addColumn": "Adicionar coluna",
+ "importLabel": "Importar",
+ "importPlaceholder": "Cole TSV (do Excel / Google Sheets) ou CSV aqui…",
+ "importButton": "Importar para a grade",
+ "pasteClipboard": "Colar da área de transferência",
+ "importEmpty": "Nada para importar — cole algumas linhas primeiro",
+ "clipboardError": "Não foi possível ler da área de transferência",
+ "clear": "Limpar",
+ "panels": {
+ "editor": "Editor de tabela",
+ "output": "Saída"
+ },
+ "align": {
+ "left": "Alinhar à esquerda",
+ "center": "Centralizar",
+ "right": "Alinhar à direita"
+ },
+ "headerPlaceholder": "Cabeçalho",
+ "cellPlaceholder": "Célula",
+ "removeRow": "Remover linha",
+ "removeColumn": "Remover coluna",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Copiar",
+ "copied": "Copiado"
+ },
+ "TimezoneConverter": {
+ "title": "Conversor de Fuso Horário",
+ "subtitle": "Converta um momento pelo mundo para planejar reuniões.",
+ "dateTimeLabel": "Data e hora",
+ "now": "Agora",
+ "sourceZoneLabel": "Fuso de origem",
+ "selectZone": "Selecionar fuso",
+ "searchZone": "Buscar fusos horários...",
+ "noZone": "Nenhum fuso horário encontrado.",
+ "scrubLabel": "Ajustar horário",
+ "reset": "Redefinir",
+ "targetsLabel": "Fusos de destino",
+ "addZone": "Adicionar fuso",
+ "remove": "Remover",
+ "localBadge": "Local",
+ "invalidDate": "Digite uma data e hora válidas.",
+ "bands": {
+ "work": "Horário comercial (09:00–18:00)",
+ "edge": "Horários limítrofes (manhã cedo / noite)",
+ "night": "Noite"
+ }
+ },
+ "TokenCounter": {
+ "title": "Contador de Tokens LLM",
+ "subtitle": "Conte tokens, visualize chunks e estime o custo de API.",
+ "inputLabel": "Texto",
+ "sample": "Exemplo",
+ "clear": "Limpar",
+ "placeholder": "Cole texto, código ou JSON para contar tokens...",
+ "encoding": "Codificação",
+ "loading": "Carregando tokenizador...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / série o)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Tokens",
+ "characters": "Caracteres",
+ "words": "Palavras",
+ "ratio": "Tokens/palavra",
+ "ratioSub": "aprox."
+ },
+ "claudeRow": "Claude (aproximado)",
+ "estimate": "estimativa",
+ "viz": {
+ "title": "Visualização de tokens",
+ "toggle": "Mostrar",
+ "hint": "Alterne para colorir cada token como um chunk.",
+ "empty": "Digite texto para visualizar os tokens.",
+ "truncated": "Mostrando os primeiros {limit} de {total} tokens."
+ },
+ "cost": {
+ "title": "Calculadora de custo",
+ "disclaimer": "valores padrão editáveis — verifique os preços atuais",
+ "model": "Modelo",
+ "expectedOutput": "Tokens de saída esperados",
+ "customInput": "Entrada $/MTok",
+ "customOutput": "Saída $/MTok",
+ "inputCost": "Custo de entrada",
+ "outputCost": "Custo de saída",
+ "total": "Total",
+ "per1000": "Por 1000 chamadas"
+ }
+ },
+ "WebhookTester": {
+ "title": "Testador de Webhook",
+ "subtitle": "Crie uma URL temporária e inspecione requisições de webhook recebidas ao vivo.",
+ "intro": {
+ "title": "Crie uma URL de webhook para começar a capturar requisições",
+ "hint": "Você receberá uma URL impossível de adivinhar que registra cada requisição recebida. Ela expira após 24 horas."
+ },
+ "createBtn": "Criar URL de webhook",
+ "creating": "Criando…",
+ "urlLabel": "Sua URL de webhook",
+ "expiresIn": "Expira em {countdown}",
+ "copyUrlBtn": "Copiar URL",
+ "copied": "Copiado",
+ "clearBtn": "Limpar",
+ "newBinBtn": "Nova URL",
+ "requestsTitle": "Requisições",
+ "selectPrompt": "Selecione uma requisição para inspecionar seus detalhes.",
+ "expiredNotice": "Esta URL de webhook expirou. Crie uma nova para continuar testando.",
+ "base64Badge": "base64",
+ "truncatedBadge": "truncado",
+ "empty": {
+ "title": "Aguardando requisições…",
+ "hint": "Envie uma requisição para sua URL e ela aparecerá aqui instantaneamente. Experimente:"
+ },
+ "detail": {
+ "receivedAt": "Recebido",
+ "source": "IP de origem",
+ "contentType": "Content-Type",
+ "query": "Parâmetros de query",
+ "headers": "Cabeçalhos",
+ "body": "Corpo",
+ "copyBody": "Copiar corpo",
+ "emptyBody": "Sem corpo"
+ },
+ "errors": {
+ "createFailed": "Falha ao criar a URL de webhook. Tente novamente.",
+ "clearFailed": "Falha ao limpar as requisições. Tente novamente."
+ }
+ },
+ "WebsocketTester": {
+ "title": "Testador de WebSocket",
+ "subtitle": "Conecte-se a um endpoint WebSocket e envie, receba e inspecione mensagens ao vivo",
+ "urlLabel": "URL do endpoint",
+ "protocolsLabel": "Subprotocolos",
+ "protocolsPlaceholder": "opcional, separados por vírgula",
+ "connect": "Conectar",
+ "disconnect": "Desconectar",
+ "status": {
+ "disconnected": "Desconectado",
+ "connecting": "Conectando",
+ "connected": "Conectado",
+ "error": "Erro"
+ },
+ "messages": {
+ "connecting": "Conectando a {url}…",
+ "connected": "Conexão estabelecida",
+ "connectedWithProtocol": "Conexão estabelecida (protocolo: {protocol})",
+ "invalidUrl": "URL inválida — deve começar com ws:// ou wss://",
+ "connectFailed": "Falha ao abrir a conexão",
+ "socketError": "Ocorreu um erro de conexão",
+ "notConnected": "Não conectado — conecte-se primeiro",
+ "binaryMessage": "Mensagem binária recebida"
+ },
+ "log": {
+ "title": "Registro de mensagens",
+ "empty": "Nenhuma mensagem ainda. Conecte-se e comece a enviar.",
+ "copyAll": "Copiar tudo",
+ "download": "Baixar como .txt",
+ "clear": "Limpar registro",
+ "prettyPrint": "Formatar JSON",
+ "showRaw": "Mostrar texto bruto"
+ },
+ "composer": {
+ "placeholder": "Digite uma mensagem para enviar…",
+ "send": "Enviar",
+ "formatJson": "Formatar como JSON",
+ "invalidJson": "A mensagem não é um JSON válido",
+ "hint": "Cmd/Ctrl+Enter para enviar"
+ },
+ "ping": {
+ "label": "Auto-ping",
+ "seconds": "seg",
+ "intervalTitle": "Intervalo de ping em segundos",
+ "payloadPlaceholder": "payload do ping"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Consulta Whois",
+ "subtitle": "Consulte detalhes de registro de qualquer domínio ou endereço IP via RDAP",
+ "placeholder": "example.com ou 8.8.8.8",
+ "lookupBtn": "Consultar",
+ "looking": "Consultando…",
+ "refreshBtn": "Atualizar",
+ "tryLabel": "Experimente:",
+ "copy": "Copiar",
+ "copied": "Copiado",
+ "notAvailable": "Não disponível",
+ "summary": {
+ "registrar": "Registrador",
+ "network": "Rede"
+ },
+ "dates": {
+ "created": "Criado",
+ "updated": "Atualizado",
+ "expires": "Expira"
+ },
+ "sections": {
+ "status": "Status",
+ "nameservers": "Servidores de nomes",
+ "contacts": "Contatos",
+ "rawRdap": "RDAP bruto"
+ },
+ "contacts": {
+ "roles": "Funções",
+ "name": "Nome",
+ "organization": "Organização"
+ },
+ "errors": {
+ "invalidTarget": "Digite um nome de domínio ou endereço IP válido",
+ "lookupFailed": "Falha na consulta (status {status})"
+ }
}
}
diff --git a/apps/web/messages/pt.json b/apps/web/messages/pt.json
index 40eccd82..52daa566 100644
--- a/apps/web/messages/pt.json
+++ b/apps/web/messages/pt.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Calculadora Chmod",
+ "codeScreenshot": "Captura de código",
+ "cssGenerators": "Geradores CSS",
+ "exifViewer": "Visualizador EXIF",
+ "faviconGenerator": "Gerador de favicon",
+ "jsonDiff": "Diff JSON",
+ "jsonpathPlayground": "Playground JSONPath",
+ "keycodeInspector": "Inspetor de keycode",
+ "markdownTableGenerator": "Tabela Markdown",
+ "timezoneConverter": "Conversor de fusos horários",
+ "tokenCounter": "Contador de tokens",
+ "webhookTester": "Testador de webhooks",
+ "websocketTester": "Testador de WebSocket",
+ "whoisLookup": "Pesquisa Whois"
},
"Help": {
"title": "Ajuda e documentação",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Gerador e verificador Bcrypt",
+ "description": "Gere hashes de palavras-passe com bcrypt e verifique hashes existentes."
+ },
+ "chmodCalculator": {
+ "title": "Calculadora Chmod",
+ "description": "Converta permissões Unix entre octal, simbólico e caixas de seleção."
+ },
+ "codeScreenshot": {
+ "title": "Captura de código",
+ "description": "Transforme código-fonte em imagens bonitas e fáceis de partilhar."
+ },
+ "cssGenerators": {
+ "title": "Geradores CSS",
+ "description": "Geradores de box-shadow, border-radius e clamp() fluido."
+ },
+ "exifViewer": {
+ "title": "Visualizador e removedor EXIF",
+ "description": "Inspecione e remova os metadados EXIF das fotos localmente."
+ },
+ "faviconGenerator": {
+ "title": "Gerador de favicon",
+ "description": "Gere favicons, ícones de app e fragmentos de manifest."
+ },
+ "jsonDiff": {
+ "title": "Diff JSON",
+ "description": "Comparação semântica e sensível à estrutura de dois documentos JSON."
+ },
+ "jsonpathPlayground": {
+ "title": "Playground JSONPath",
+ "description": "Consulte JSON com JSONPath ou JMESPath e veja os resultados em tempo real."
+ },
+ "keycodeInspector": {
+ "title": "Inspetor de keycode",
+ "description": "Prima uma tecla para ver os seus códigos de evento JavaScript e modificadores."
+ },
+ "markdownTableGenerator": {
+ "title": "Gerador de tabelas Markdown",
+ "description": "Crie tabelas Markdown e HTML num editor de grelha semelhante a uma folha de cálculo."
+ },
+ "timezoneConverter": {
+ "title": "Conversor de fusos horários",
+ "description": "Converta uma hora entre vários fusos com indicações de mudança de dia e horário de trabalho."
+ },
+ "tokenCounter": {
+ "title": "Contador de tokens de LLM",
+ "description": "Conte tokens de LLM, visualize chunks e estime o custo da API."
+ },
+ "webhookTester": {
+ "title": "Testador de webhooks",
+ "description": "Capture e inspecione pedidos de webhook recebidos em tempo real."
+ },
+ "websocketTester": {
+ "title": "Testador de WebSocket",
+ "description": "Ligue-se a endpoints WebSocket e troque mensagens em tempo real."
+ },
+ "whoisLookup": {
+ "title": "Pesquisa Whois",
+ "description": "Consulte os dados de registo de um domínio ou IP através de RDAP."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Gerador e verificador Bcrypt",
+ "subtitle": "Gere hashes de palavras-passe com bcrypt e verifique hashes — tudo permanece no seu browser",
+ "tabs": {
+ "hash": "Hash",
+ "verify": "Verificar"
+ },
+ "showPassword": "Mostrar palavra-passe",
+ "hidePassword": "Ocultar palavra-passe",
+ "copy": "Copiar",
+ "copied": "Copiado",
+ "hash": {
+ "passwordLabel": "Palavra-passe",
+ "passwordPlaceholder": "Introduza uma palavra-passe para gerar o hash…",
+ "costLabel": "Fator de custo",
+ "costHint": "Um custo mais alto duplica o trabalho por passo. 10-12 é uma escolha comum em produção.",
+ "slowWarning": "Fatores de custo de 13 ou superiores podem demorar vários segundos a calcular.",
+ "generate": "Gerar hash",
+ "generating": "A gerar hash…",
+ "resultLabel": "Hash bcrypt",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "Anatomia do hash",
+ "algorithm": "algoritmo",
+ "cost": "custo",
+ "salt": "salt (22 caracteres)",
+ "hash": "hash (31 caracteres)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Palavra-passe",
+ "passwordPlaceholder": "Introduza a palavra-passe a verificar…",
+ "hashLabel": "Hash bcrypt",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Introduza uma palavra-passe e um hash bcrypt para os comparar.",
+ "invalidHash": "Isto não parece um hash bcrypt válido.",
+ "checking": "A comparar…",
+ "match": "Corresponde — a palavra-passe produz este hash.",
+ "noMatch": "Não corresponde — a palavra-passe não produz este hash."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Calculadora Chmod",
+ "subtitle": "Converta permissões de ficheiros Unix entre caixas de seleção, octal e notação simbólica",
+ "grid": {
+ "title": "Permissões",
+ "read": "Leitura",
+ "write": "Escrita",
+ "execute": "Execução",
+ "owner": "Proprietário",
+ "group": "Grupo",
+ "others": "Outros"
+ },
+ "special": {
+ "title": "Bits especiais",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "Octal",
+ "placeholder": "755 ou 4755",
+ "invalid": "Introduza 3 ou 4 dígitos octais (0-7)."
+ },
+ "symbolic": {
+ "label": "Simbólico",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Introduza 9 caracteres como rwxr-xr-x (s/S, t/T permitidos)."
+ },
+ "output": {
+ "title": "Comando",
+ "copy": "Copiar",
+ "copied": "Copiado"
+ },
+ "explain": {
+ "line": "{cls} pode {perms}.",
+ "none": "{cls} não tem permissões.",
+ "setuid": "Setuid: o ficheiro é executado com os privilégios do proprietário.",
+ "setgid": "Setgid: o ficheiro é executado com os privilégios do seu grupo (os ficheiros novos numa diretoria herdam o seu grupo).",
+ "sticky": "Sticky: apenas o proprietário de um ficheiro o pode eliminar ou renomear dentro desta diretoria."
+ },
+ "presets": {
+ "title": "Predefinições comuns",
+ "octal": "Octal",
+ "permissions": "Permissões",
+ "description": "Uso típico",
+ "p644": "Ficheiros normais: o proprietário escreve, todos leem.",
+ "p755": "Executáveis e diretorias: todos podem ler e entrar.",
+ "p600": "Ficheiros privados: apenas o proprietário lê e escreve.",
+ "p700": "Executáveis e diretorias privados: apenas o proprietário.",
+ "p664": "Ficheiros com escrita para o grupo.",
+ "p775": "Diretorias e executáveis com escrita para o grupo.",
+ "p400": "Apenas leitura para o proprietário (ex.: chaves privadas SSH)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Captura de código",
+ "subtitle": "Transforme código em imagens bonitas e fáceis de partilhar",
+ "languageLabel": "Linguagem",
+ "autoDetect": "Deteção automática",
+ "backgroundLabel": "Fundo",
+ "paddingLabel": "Espaçamento",
+ "filenameLabel": "Nome do ficheiro",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "Pontos da janela",
+ "themeLabel": "Tema da janela",
+ "themeDark": "Escuro",
+ "themeLight": "Claro",
+ "scaleLabel": "Escala de exportação",
+ "copyImage": "Copiar imagem",
+ "copied": "Copiado",
+ "exportPng": "Exportar PNG",
+ "inputLabel": "Código",
+ "detected": "Detetado: {language}",
+ "clear": "Limpar",
+ "placeholder": "Cole o seu código aqui…",
+ "previewLabel": "Pré-visualização",
+ "emptyPreview": "A pré-visualização do seu código aparecerá aqui",
+ "exportFailed": "Falha ao exportar a imagem",
+ "copyUnsupported": "Copiar imagens não é suportado neste browser",
+ "copyFailed": "Falha ao copiar a imagem",
+ "backgrounds": {
+ "ocean": "Oceano",
+ "sunset": "Pôr do sol",
+ "candy": "Rebuçado",
+ "forest": "Floresta",
+ "midnight": "Meia-noite",
+ "gold": "Ouro",
+ "aurora": "Aurora",
+ "slate": "Ardósia",
+ "transparent": "Transparente"
+ }
+ },
+ "CssGenerators": {
+ "title": "Geradores CSS",
+ "subtitle": "Box-shadow, border-radius e clamp() fluido — copie o CSS.",
+ "preview": "Pré-visualização",
+ "output": "CSS",
+ "copy": "Copiar",
+ "copied": "Copiado",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "clamp() fluido"
+ },
+ "boxShadow": {
+ "presets": "Predefinições",
+ "layer": "Camada",
+ "addLayer": "Adicionar camada",
+ "removeLayer": "Remover camada",
+ "x": "X",
+ "y": "Y",
+ "blur": "Desfoque",
+ "spread": "Expansão",
+ "color": "Cor",
+ "alpha": "Alfa",
+ "inset": "Interior",
+ "presetNames": {
+ "subtleCard": "Cartão subtil",
+ "elevated": "Elevado",
+ "hardOffset": "Deslocamento nítido",
+ "neonGlow": "Brilho néon",
+ "inner": "Interior",
+ "layeredSmooth": "Camadas suaves"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Modo",
+ "modes": {
+ "simple": "Por canto",
+ "blob": "Blob orgânico"
+ },
+ "unit": "Unidade",
+ "linkCorners": "Ligar cantos",
+ "randomize": "Aleatorizar",
+ "horizontal": "Raios horizontais",
+ "vertical": "Raios verticais",
+ "corners": {
+ "tl": "Superior esquerdo",
+ "tr": "Superior direito",
+ "br": "Inferior direito",
+ "bl": "Inferior esquerdo"
+ }
+ },
+ "clamp": {
+ "minFont": "Fonte mín. (px)",
+ "maxFont": "Fonte máx. (px)",
+ "minViewport": "Viewport mín. (px)",
+ "maxViewport": "Viewport máx. (px)",
+ "rootFont": "Fonte raiz (px)",
+ "viewportWidth": "Viewport simulado",
+ "computedSize": "Tamanho calculado",
+ "slope": "Declive",
+ "intercept": "Interceção",
+ "previewText": "Um pequeno jabuti xereta",
+ "invalid": "Introduza limites de viewport válidos e diferentes."
+ }
+ },
+ "ExifViewer": {
+ "title": "Visualizador e removedor EXIF",
+ "subtitle": "Inspecione os metadados das fotos e remova-os no seu browser",
+ "dropTitle": "Largue uma imagem aqui",
+ "dropHint": "ou clique para procurar — a sua imagem nunca sai do seu dispositivo",
+ "browse": "Escolher imagem",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "A ler metadados…",
+ "parseFailed": "Falha ao ler esta imagem",
+ "clear": "Remover imagem",
+ "gpsWarning": "Contém localização GPS",
+ "openMap": "Ver no OpenStreetMap",
+ "noMetadata": "Não foram encontrados metadados incorporados nesta imagem.",
+ "facts": {
+ "name": "Nome",
+ "type": "Tipo",
+ "dimensions": "Dimensões",
+ "size": "Tamanho"
+ },
+ "groups": {
+ "camera": "Câmara",
+ "exposure": "Exposição",
+ "timestamps": "Datas/horas",
+ "gps": "Localização GPS",
+ "software": "Software e outros"
+ },
+ "gpsCard": {
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "altitude": "Altitude"
+ },
+ "raw": {
+ "title": "Todos os metadados",
+ "copyJson": "Copiar JSON",
+ "copied": "Copiado"
+ },
+ "remove": {
+ "title": "Remover metadados",
+ "jpegDescription": "Remova sem perdas EXIF, XMP, IPTC e comentários sem recomprimir a imagem.",
+ "reencodeDescription": "Recodifique a imagem para remover os metadados incorporados. Isto recomprime os pixels.",
+ "keepIcc": "Manter o perfil de cor (ICC)",
+ "stripButton": "Remover metadados",
+ "reencodeButton": "Recodificar e limpar",
+ "unsupported": "A remoção de metadados não é suportada para este tipo de ficheiro.",
+ "failed": "Falha ao remover os metadados",
+ "before": "Antes",
+ "after": "Depois",
+ "download": "Descarregar imagem limpa"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Gerador de favicon",
+ "subtitle": "Crie favicons e ícones de app a partir de uma única imagem",
+ "dropTitle": "Largue uma imagem aqui",
+ "dropHint": "PNG, JPEG ou SVG — clique para procurar",
+ "browse": "Escolher imagem",
+ "clear": "Remover imagem",
+ "backgroundLabel": "Fundo",
+ "transparent": "Transparente",
+ "solidColor": "Sólido",
+ "cornerLabel": "Forma dos cantos",
+ "cornerNone": "Quadrado",
+ "cornerRounded": "Arredondado",
+ "cornerCircle": "Círculo",
+ "maskPaddingLabel": "Espaçamento maskable",
+ "maskable": "maskable",
+ "previewLabel": "Pré-visualização",
+ "copy": "Copiar",
+ "headSnippet": "Fragmento para o head HTML",
+ "downloadIco": "Descarregar favicon.ico",
+ "downloadZip": "Descarregar tudo (.zip)"
+ },
+ "JsonDiff": {
+ "title": "Diff JSON",
+ "subtitle": "Comparação semântica e sensível à estrutura de dois documentos JSON — ordem das chaves e formatação ignoradas",
+ "options": {
+ "ignoreArrayOrder": "Ignorar a ordem dos arrays"
+ },
+ "loadBothSamples": "Carregar exemplos",
+ "sample": "Carregar exemplo",
+ "format": "Formatar",
+ "clear": "Limpar",
+ "formatError": "Não é possível formatar: a entrada não é um JSON válido",
+ "placeholder": "Cole ou escreva JSON aqui…",
+ "panels": {
+ "left": "Original (esquerda)",
+ "right": "Modificado (direita)",
+ "results": "Diferenças"
+ },
+ "summary": {
+ "added": "{count} adicionados",
+ "removed": "{count} removidos",
+ "changed": "{count} alterados"
+ },
+ "types": {
+ "added": "Adicionado",
+ "removed": "Removido",
+ "changed": "Alterado"
+ },
+ "copyChanges": "Copiar como JSON",
+ "copied": "Copiado",
+ "empty": "Cole JSON em ambos os painéis para ver as suas diferenças.",
+ "identical": "Os dois documentos são semanticamente idênticos."
+ },
+ "JsonpathPlayground": {
+ "title": "Playground JSONPath",
+ "subtitle": "Consulte documentos JSON com JSONPath ou JMESPath e veja os resultados em tempo real",
+ "engineLabel": "Motor",
+ "queryLabel": "Consulta",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Referência rápida",
+ "cheats": {
+ "jpAllTitles": "Todos os títulos de livros",
+ "jpAllAuthors": "Todos os autores, em qualquer parte do documento",
+ "jpFirstBook": "Primeiro livro",
+ "jpLastBook": "Último livro",
+ "jpSlice": "Os dois primeiros livros (slice)",
+ "jpFilterPrice": "Livros abaixo de 40",
+ "jpFilterInStock": "Títulos dos livros em stock",
+ "jpRecursive": "Todos os valores, recursivamente",
+ "jmAllTitles": "Todos os títulos de livros",
+ "jmFirstAuthor": "Autor do primeiro livro",
+ "jmLastBook": "Último livro",
+ "jmSlice": "Os dois primeiros livros (slice)",
+ "jmFilterPrice": "Títulos dos livros abaixo de 40",
+ "jmCount": "Contar livros em stock",
+ "jmProjection": "Reformular cada livro num novo objeto",
+ "jmSort": "Títulos ordenados por preço"
+ },
+ "panels": {
+ "input": "Entrada JSON",
+ "results": "Resultados"
+ },
+ "sample": "Carregar exemplo",
+ "format": "Formatar",
+ "clear": "Limpar",
+ "formatError": "Não é possível formatar: a entrada não é um JSON válido",
+ "placeholder": "Cole ou escreva JSON aqui…",
+ "outputPlaceholder": "As correspondências aparecerão aqui…",
+ "matchCount": "{count, plural, one {# correspondência} other {# correspondências}}",
+ "copy": "Copiar",
+ "copied": "Copiado"
+ },
+ "KeycodeInspector": {
+ "title": "Inspetor de keycode",
+ "subtitle": "Prima qualquer tecla para inspecionar o seu evento de teclado JavaScript",
+ "capture": {
+ "prompt": "Prima qualquer tecla…",
+ "ready": "A escutar — prima qualquer tecla no seu teclado",
+ "clickToFocus": "Clique aqui para focar e depois prima qualquer tecla"
+ },
+ "hero": {
+ "spaceKey": "(Espaço)",
+ "keyLabel": "event.key",
+ "repeat": "repetição"
+ },
+ "copyJson": "Copiar JSON",
+ "copiedJson": "JSON do evento copiado para a área de transferência",
+ "deprecated": "obsoleto",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Padrão",
+ "left": "Esquerda",
+ "right": "Direita",
+ "numpad": "Teclado numérico",
+ "unknown": "Desconhecido"
+ },
+ "history": {
+ "title": "Histórico",
+ "clear": "Limpar histórico",
+ "empty": "As teclas premidas aparecerão aqui."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Gerador de tabelas Markdown",
+ "subtitle": "Edite uma grelha, cole de uma folha de cálculo e copie uma tabela Markdown ou HTML bem alinhada",
+ "addRow": "Adicionar linha",
+ "addColumn": "Adicionar coluna",
+ "importLabel": "Importar",
+ "importPlaceholder": "Cole TSV (do Excel / Google Sheets) ou CSV aqui…",
+ "importButton": "Importar para a grelha",
+ "pasteClipboard": "Colar da área de transferência",
+ "importEmpty": "Nada para importar — cole primeiro algumas linhas",
+ "clipboardError": "Não foi possível ler a área de transferência",
+ "clear": "Limpar",
+ "panels": {
+ "editor": "Editor de tabelas",
+ "output": "Saída"
+ },
+ "align": {
+ "left": "Alinhar à esquerda",
+ "center": "Centrar",
+ "right": "Alinhar à direita"
+ },
+ "headerPlaceholder": "Cabeçalho",
+ "cellPlaceholder": "Célula",
+ "removeRow": "Remover linha",
+ "removeColumn": "Remover coluna",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Copiar",
+ "copied": "Copiado"
+ },
+ "TimezoneConverter": {
+ "title": "Conversor de fusos horários",
+ "subtitle": "Converta um momento por todo o mundo para planear reuniões.",
+ "dateTimeLabel": "Data e hora",
+ "now": "Agora",
+ "sourceZoneLabel": "Fuso horário de origem",
+ "selectZone": "Selecionar fuso",
+ "searchZone": "Procurar fusos horários...",
+ "noZone": "Nenhum fuso horário encontrado.",
+ "scrubLabel": "Deslocar a hora",
+ "reset": "Repor",
+ "targetsLabel": "Fusos de destino",
+ "addZone": "Adicionar fuso",
+ "remove": "Remover",
+ "localBadge": "Local",
+ "invalidDate": "Introduza uma data e hora válidas.",
+ "bands": {
+ "work": "Horário de trabalho (09:00–18:00)",
+ "edge": "Horas limite (cedo / noite)",
+ "night": "Noite"
+ }
+ },
+ "TokenCounter": {
+ "title": "Contador de tokens de LLM",
+ "subtitle": "Conte tokens, visualize chunks e estime o custo da API.",
+ "inputLabel": "Texto",
+ "sample": "Carregar exemplo",
+ "clear": "Limpar",
+ "placeholder": "Cole texto, código ou JSON para contar tokens...",
+ "encoding": "Codificação",
+ "loading": "A carregar o tokenizador...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / série o)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Tokens",
+ "characters": "Caracteres",
+ "words": "Palavras",
+ "ratio": "Tokens/palavra",
+ "ratioSub": "aprox."
+ },
+ "claudeRow": "Claude (aproximado)",
+ "estimate": "estimativa",
+ "viz": {
+ "title": "Visualização de tokens",
+ "toggle": "Mostrar",
+ "hint": "Ative para colorir cada token como um chunk.",
+ "empty": "Introduza texto para visualizar os tokens.",
+ "truncated": "A mostrar os primeiros {limit} de {total} tokens."
+ },
+ "cost": {
+ "title": "Calculadora de custos",
+ "disclaimer": "valores predefinidos editáveis — verifique os preços atuais",
+ "model": "Modelo",
+ "expectedOutput": "Tokens de saída esperados",
+ "customInput": "Entrada $/MTok",
+ "customOutput": "Saída $/MTok",
+ "inputCost": "Custo de entrada",
+ "outputCost": "Custo de saída",
+ "total": "Total",
+ "per1000": "Por 1000 chamadas"
+ }
+ },
+ "WebhookTester": {
+ "title": "Testador de webhooks",
+ "subtitle": "Crie um URL temporário e inspecione os pedidos de webhook recebidos em tempo real.",
+ "intro": {
+ "title": "Crie um URL de webhook para começar a capturar pedidos",
+ "hint": "Vai obter um URL indecifrável que regista cada pedido recebido. Expira ao fim de 24 horas."
+ },
+ "createBtn": "Criar URL de webhook",
+ "creating": "A criar…",
+ "urlLabel": "O seu URL de webhook",
+ "expiresIn": "Expira em {countdown}",
+ "copyUrlBtn": "Copiar URL",
+ "copied": "Copiado",
+ "clearBtn": "Limpar",
+ "newBinBtn": "Novo URL",
+ "requestsTitle": "Pedidos",
+ "selectPrompt": "Selecione um pedido para inspecionar os seus detalhes.",
+ "expiredNotice": "Este URL de webhook expirou. Crie um novo para continuar a testar.",
+ "base64Badge": "base64",
+ "truncatedBadge": "truncado",
+ "empty": {
+ "title": "À espera de pedidos…",
+ "hint": "Envie um pedido para o seu URL e ele aparecerá aqui instantaneamente. Experimente isto:"
+ },
+ "detail": {
+ "receivedAt": "Recebido",
+ "source": "IP de origem",
+ "contentType": "Content-Type",
+ "query": "Parâmetros de consulta",
+ "headers": "Cabeçalhos",
+ "body": "Corpo",
+ "copyBody": "Copiar corpo",
+ "emptyBody": "Sem corpo"
+ },
+ "errors": {
+ "createFailed": "Falha ao criar o URL de webhook. Tente novamente.",
+ "clearFailed": "Falha ao limpar os pedidos. Tente novamente."
+ }
+ },
+ "WebsocketTester": {
+ "title": "Testador de WebSocket",
+ "subtitle": "Ligue-se a um endpoint WebSocket e envie, receba e inspecione mensagens em tempo real",
+ "urlLabel": "URL do endpoint",
+ "protocolsLabel": "Subprotocolos",
+ "protocolsPlaceholder": "opcional, separados por vírgulas",
+ "connect": "Ligar",
+ "disconnect": "Desligar",
+ "status": {
+ "disconnected": "Desligado",
+ "connecting": "A ligar",
+ "connected": "Ligado",
+ "error": "Erro"
+ },
+ "messages": {
+ "connecting": "A ligar a {url}…",
+ "connected": "Ligação estabelecida",
+ "connectedWithProtocol": "Ligação estabelecida (protocolo: {protocol})",
+ "invalidUrl": "URL inválido — deve começar por ws:// ou wss://",
+ "connectFailed": "Falha ao abrir a ligação",
+ "socketError": "Ocorreu um erro de ligação",
+ "notConnected": "Não ligado — ligue-se primeiro",
+ "binaryMessage": "Recebida uma mensagem binária"
+ },
+ "log": {
+ "title": "Registo de mensagens",
+ "empty": "Ainda não há mensagens. Ligue-se e comece a enviar.",
+ "copyAll": "Copiar tudo",
+ "download": "Descarregar como .txt",
+ "clear": "Limpar registo",
+ "prettyPrint": "Formatar JSON",
+ "showRaw": "Mostrar texto simples"
+ },
+ "composer": {
+ "placeholder": "Escreva uma mensagem para enviar…",
+ "send": "Enviar",
+ "formatJson": "Formatar como JSON",
+ "invalidJson": "A mensagem não é um JSON válido",
+ "hint": "Cmd/Ctrl+Enter para enviar"
+ },
+ "ping": {
+ "label": "Auto-ping",
+ "seconds": "s",
+ "intervalTitle": "Intervalo de ping em segundos",
+ "payloadPlaceholder": "payload do ping"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Pesquisa Whois",
+ "subtitle": "Consulte os dados de registo de qualquer domínio ou endereço IP através de RDAP",
+ "placeholder": "example.com ou 8.8.8.8",
+ "lookupBtn": "Pesquisar",
+ "looking": "A pesquisar…",
+ "refreshBtn": "Atualizar",
+ "tryLabel": "Experimente:",
+ "copy": "Copiar",
+ "copied": "Copiado",
+ "notAvailable": "Não disponível",
+ "summary": {
+ "registrar": "Registrar",
+ "network": "Rede"
+ },
+ "dates": {
+ "created": "Criado",
+ "updated": "Atualizado",
+ "expires": "Expira"
+ },
+ "sections": {
+ "status": "Estado",
+ "nameservers": "Servidores de nomes",
+ "contacts": "Contactos",
+ "rawRdap": "RDAP em bruto"
+ },
+ "contacts": {
+ "roles": "Funções",
+ "name": "Nome",
+ "organization": "Organização"
+ },
+ "errors": {
+ "invalidTarget": "Introduza um nome de domínio ou endereço IP válido",
+ "lookupFailed": "Pesquisa falhou (estado {status})"
+ }
}
}
diff --git a/apps/web/messages/ru.json b/apps/web/messages/ru.json
index 9d4ac723..fdc7c6ef 100644
--- a/apps/web/messages/ru.json
+++ b/apps/web/messages/ru.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Калькулятор Chmod",
+ "codeScreenshot": "Скриншот кода",
+ "cssGenerators": "Генераторы CSS",
+ "exifViewer": "Просмотр EXIF",
+ "faviconGenerator": "Генератор favicon",
+ "jsonDiff": "Сравнение JSON",
+ "jsonpathPlayground": "Песочница JSONPath",
+ "keycodeInspector": "Инспектор клавиш",
+ "markdownTableGenerator": "Таблица Markdown",
+ "timezoneConverter": "Конвертер часовых поясов",
+ "tokenCounter": "Счётчик токенов",
+ "webhookTester": "Тестер вебхуков",
+ "websocketTester": "Тестер WebSocket",
+ "whoisLookup": "Whois-запрос"
},
"Help": {
"title": "Справка и документация",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Генератор и проверка Bcrypt",
+ "description": "Хешируйте пароли с помощью bcrypt и проверяйте существующие хеши."
+ },
+ "chmodCalculator": {
+ "title": "Калькулятор Chmod",
+ "description": "Преобразуйте права Unix между восьмеричной, символьной формой и флажками."
+ },
+ "codeScreenshot": {
+ "title": "Скриншот кода",
+ "description": "Превращайте исходный код в красивые изображения для публикации."
+ },
+ "cssGenerators": {
+ "title": "Генераторы CSS",
+ "description": "Генераторы box-shadow, border-radius и адаптивного clamp()."
+ },
+ "exifViewer": {
+ "title": "Просмотр и удаление EXIF",
+ "description": "Просматривайте и удаляйте метаданные EXIF из фото локально."
+ },
+ "faviconGenerator": {
+ "title": "Генератор favicon",
+ "description": "Создавайте favicon, иконки приложений и фрагменты манифеста."
+ },
+ "jsonDiff": {
+ "title": "Сравнение JSON",
+ "description": "Семантическое, учитывающее структуру сравнение двух документов JSON."
+ },
+ "jsonpathPlayground": {
+ "title": "Песочница JSONPath",
+ "description": "Запрашивайте JSON с помощью JSONPath или JMESPath и смотрите результаты вживую."
+ },
+ "keycodeInspector": {
+ "title": "Инспектор клавиш",
+ "description": "Нажмите клавишу, чтобы увидеть её коды события JavaScript и модификаторы."
+ },
+ "markdownTableGenerator": {
+ "title": "Генератор таблиц Markdown",
+ "description": "Создавайте таблицы Markdown и HTML в табличном редакторе."
+ },
+ "timezoneConverter": {
+ "title": "Конвертер часовых поясов",
+ "description": "Преобразуйте время между поясами с подсказками о смене дня и рабочих часах."
+ },
+ "tokenCounter": {
+ "title": "Счётчик токенов LLM",
+ "description": "Считайте токены LLM, визуализируйте чанки и оценивайте стоимость API."
+ },
+ "webhookTester": {
+ "title": "Тестер вебхуков",
+ "description": "Захватывайте и изучайте входящие запросы вебхуков в реальном времени."
+ },
+ "websocketTester": {
+ "title": "Тестер WebSocket",
+ "description": "Подключайтесь к эндпоинтам WebSocket и обменивайтесь сообщениями в реальном времени."
+ },
+ "whoisLookup": {
+ "title": "Whois-запрос",
+ "description": "Узнавайте данные регистрации доменов и IP через RDAP."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Генератор и проверка Bcrypt",
+ "subtitle": "Хешируйте пароли с помощью bcrypt и проверяйте хеши — всё остаётся в вашем браузере",
+ "tabs": {
+ "hash": "Хеш",
+ "verify": "Проверка"
+ },
+ "showPassword": "Показать пароль",
+ "hidePassword": "Скрыть пароль",
+ "copy": "Копировать",
+ "copied": "Скопировано",
+ "hash": {
+ "passwordLabel": "Пароль",
+ "passwordPlaceholder": "Введите пароль для хеширования…",
+ "costLabel": "Фактор стоимости",
+ "costHint": "Более высокий фактор удваивает работу на каждом шаге. 10-12 — распространённый выбор для продакшена.",
+ "slowWarning": "Факторы стоимости 13 и выше могут вычисляться несколько секунд.",
+ "generate": "Создать хеш",
+ "generating": "Хеширование…",
+ "resultLabel": "Хеш bcrypt",
+ "timeTaken": "{ms} мс",
+ "anatomy": {
+ "title": "Строение хеша",
+ "algorithm": "алгоритм",
+ "cost": "стоимость",
+ "salt": "соль (22 символа)",
+ "hash": "хеш (31 символ)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Пароль",
+ "passwordPlaceholder": "Введите пароль для проверки…",
+ "hashLabel": "Хеш bcrypt",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Введите пароль и хеш bcrypt, чтобы сравнить их.",
+ "invalidHash": "Это не похоже на корректный хеш bcrypt.",
+ "checking": "Сравнение…",
+ "match": "Совпадает — пароль даёт этот хеш.",
+ "noMatch": "Не совпадает — пароль не даёт этот хеш."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Калькулятор Chmod",
+ "subtitle": "Преобразуйте права файлов Unix между флажками, восьмеричной и символьной формой",
+ "grid": {
+ "title": "Права",
+ "read": "Чтение",
+ "write": "Запись",
+ "execute": "Выполнение",
+ "owner": "Владелец",
+ "group": "Группа",
+ "others": "Остальные"
+ },
+ "special": {
+ "title": "Специальные биты",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "Восьмеричная",
+ "placeholder": "755 или 4755",
+ "invalid": "Введите 3 или 4 восьмеричные цифры (0-7)."
+ },
+ "symbolic": {
+ "label": "Символьная",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Введите 9 символов вида rwxr-xr-x (допустимы s/S, t/T)."
+ },
+ "output": {
+ "title": "Команда",
+ "copy": "Копировать",
+ "copied": "Скопировано"
+ },
+ "explain": {
+ "line": "{cls} может {perms}.",
+ "none": "{cls} не имеет прав.",
+ "setuid": "Setuid: файл выполняется с привилегиями своего владельца.",
+ "setgid": "Setgid: файл выполняется с привилегиями своей группы (новые файлы в каталоге наследуют его группу).",
+ "sticky": "Sticky: только владелец файла может удалить или переименовать его в этом каталоге."
+ },
+ "presets": {
+ "title": "Частые пресеты",
+ "octal": "Восьмеричная",
+ "permissions": "Права",
+ "description": "Типичное применение",
+ "p644": "Обычные файлы: владелец пишет, все читают.",
+ "p755": "Исполняемые файлы и каталоги: все могут читать и входить.",
+ "p600": "Приватные файлы: только владелец читает и пишет.",
+ "p700": "Приватные исполняемые файлы и каталоги: только владелец.",
+ "p664": "Файлы, доступные группе для записи.",
+ "p775": "Каталоги и исполняемые файлы, доступные группе для записи.",
+ "p400": "Только чтение для владельца (например, закрытые ключи SSH)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Скриншот кода",
+ "subtitle": "Превращайте код в красивые изображения для публикации",
+ "languageLabel": "Язык",
+ "autoDetect": "Автоопределение",
+ "backgroundLabel": "Фон",
+ "paddingLabel": "Отступ",
+ "filenameLabel": "Имя файла",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "Точки окна",
+ "themeLabel": "Тема окна",
+ "themeDark": "Тёмная",
+ "themeLight": "Светлая",
+ "scaleLabel": "Масштаб экспорта",
+ "copyImage": "Копировать изображение",
+ "copied": "Скопировано",
+ "exportPng": "Экспорт PNG",
+ "inputLabel": "Код",
+ "detected": "Определено: {language}",
+ "clear": "Очистить",
+ "placeholder": "Вставьте ваш код сюда…",
+ "previewLabel": "Предпросмотр",
+ "emptyPreview": "Здесь появится предпросмотр вашего кода",
+ "exportFailed": "Не удалось экспортировать изображение",
+ "copyUnsupported": "Копирование изображений не поддерживается в этом браузере",
+ "copyFailed": "Не удалось скопировать изображение",
+ "backgrounds": {
+ "ocean": "Океан",
+ "sunset": "Закат",
+ "candy": "Конфета",
+ "forest": "Лес",
+ "midnight": "Полночь",
+ "gold": "Золото",
+ "aurora": "Аврора",
+ "slate": "Сланец",
+ "transparent": "Прозрачный"
+ }
+ },
+ "CssGenerators": {
+ "title": "Генераторы CSS",
+ "subtitle": "Box-shadow, border-radius и адаптивный clamp() — копируйте CSS.",
+ "preview": "Предпросмотр",
+ "output": "CSS",
+ "copy": "Копировать",
+ "copied": "Скопировано",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "Адаптивный clamp()"
+ },
+ "boxShadow": {
+ "presets": "Пресеты",
+ "layer": "Слой",
+ "addLayer": "Добавить слой",
+ "removeLayer": "Удалить слой",
+ "x": "X",
+ "y": "Y",
+ "blur": "Размытие",
+ "spread": "Растяжение",
+ "color": "Цвет",
+ "alpha": "Альфа",
+ "inset": "Внутренняя",
+ "presetNames": {
+ "subtleCard": "Лёгкая карточка",
+ "elevated": "Приподнятая",
+ "hardOffset": "Жёсткое смещение",
+ "neonGlow": "Неоновое свечение",
+ "inner": "Внутренняя",
+ "layeredSmooth": "Мягкие слои"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Режим",
+ "modes": {
+ "simple": "По углам",
+ "blob": "Органичная капля"
+ },
+ "unit": "Единица",
+ "linkCorners": "Связать углы",
+ "randomize": "Случайно",
+ "horizontal": "Горизонтальные радиусы",
+ "vertical": "Вертикальные радиусы",
+ "corners": {
+ "tl": "Верхний левый",
+ "tr": "Верхний правый",
+ "br": "Нижний правый",
+ "bl": "Нижний левый"
+ }
+ },
+ "clamp": {
+ "minFont": "Мин. шрифт (px)",
+ "maxFont": "Макс. шрифт (px)",
+ "minViewport": "Мин. viewport (px)",
+ "maxViewport": "Макс. viewport (px)",
+ "rootFont": "Корневой шрифт (px)",
+ "viewportWidth": "Смоделированный viewport",
+ "computedSize": "Вычисленный размер",
+ "slope": "Наклон",
+ "intercept": "Смещение",
+ "previewText": "Быстрая рыжая лиса",
+ "invalid": "Введите корректные и различающиеся границы viewport."
+ }
+ },
+ "ExifViewer": {
+ "title": "Просмотр и удаление EXIF",
+ "subtitle": "Изучайте метаданные фото и удаляйте их в вашем браузере",
+ "dropTitle": "Перетащите изображение сюда",
+ "dropHint": "или нажмите для выбора — ваше изображение никогда не покидает устройство",
+ "browse": "Выбрать изображение",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Чтение метаданных…",
+ "parseFailed": "Не удалось прочитать это изображение",
+ "clear": "Удалить изображение",
+ "gpsWarning": "Содержит GPS-координаты",
+ "openMap": "Показать на OpenStreetMap",
+ "noMetadata": "В этом изображении не найдено встроенных метаданных.",
+ "facts": {
+ "name": "Имя",
+ "type": "Тип",
+ "dimensions": "Размеры",
+ "size": "Размер"
+ },
+ "groups": {
+ "camera": "Камера",
+ "exposure": "Экспозиция",
+ "timestamps": "Метки времени",
+ "gps": "GPS-координаты",
+ "software": "ПО и прочее"
+ },
+ "gpsCard": {
+ "latitude": "Широта",
+ "longitude": "Долгота",
+ "altitude": "Высота"
+ },
+ "raw": {
+ "title": "Все метаданные",
+ "copyJson": "Копировать JSON",
+ "copied": "Скопировано"
+ },
+ "remove": {
+ "title": "Удалить метаданные",
+ "jpegDescription": "Без потерь удалите EXIF, XMP, IPTC и комментарии без повторного сжатия изображения.",
+ "reencodeDescription": "Перекодируйте изображение, чтобы удалить встроенные метаданные. При этом пиксели пересжимаются.",
+ "keepIcc": "Сохранить цветовой профиль (ICC)",
+ "stripButton": "Удалить метаданные",
+ "reencodeButton": "Перекодировать и удалить",
+ "unsupported": "Удаление метаданных не поддерживается для этого типа файла.",
+ "failed": "Не удалось удалить метаданные",
+ "before": "До",
+ "after": "После",
+ "download": "Скачать очищенное изображение"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Генератор favicon",
+ "subtitle": "Создавайте favicon и иконки приложений из одного изображения",
+ "dropTitle": "Перетащите изображение сюда",
+ "dropHint": "PNG, JPEG или SVG — нажмите для выбора",
+ "browse": "Выбрать изображение",
+ "clear": "Удалить изображение",
+ "backgroundLabel": "Фон",
+ "transparent": "Прозрачный",
+ "solidColor": "Сплошной",
+ "cornerLabel": "Форма углов",
+ "cornerNone": "Квадрат",
+ "cornerRounded": "Скруглённый",
+ "cornerCircle": "Круг",
+ "maskPaddingLabel": "Отступ maskable",
+ "maskable": "maskable",
+ "previewLabel": "Предпросмотр",
+ "copy": "Копировать",
+ "headSnippet": "Фрагмент для head HTML",
+ "downloadIco": "Скачать favicon.ico",
+ "downloadZip": "Скачать всё (.zip)"
+ },
+ "JsonDiff": {
+ "title": "Сравнение JSON",
+ "subtitle": "Семантическое, учитывающее структуру сравнение двух документов JSON — порядок ключей и форматирование игнорируются",
+ "options": {
+ "ignoreArrayOrder": "Игнорировать порядок массивов"
+ },
+ "loadBothSamples": "Загрузить примеры",
+ "sample": "Пример",
+ "format": "Формат",
+ "clear": "Очистить",
+ "formatError": "Не удаётся отформатировать: ввод не является корректным JSON",
+ "placeholder": "Вставьте или введите JSON здесь…",
+ "panels": {
+ "left": "Оригинал (слева)",
+ "right": "Изменённый (справа)",
+ "results": "Различия"
+ },
+ "summary": {
+ "added": "{count} добавлено",
+ "removed": "{count} удалено",
+ "changed": "{count} изменено"
+ },
+ "types": {
+ "added": "Добавлено",
+ "removed": "Удалено",
+ "changed": "Изменено"
+ },
+ "copyChanges": "Копировать как JSON",
+ "copied": "Скопировано",
+ "empty": "Вставьте JSON в обе панели, чтобы увидеть их различия.",
+ "identical": "Оба документа семантически идентичны."
+ },
+ "JsonpathPlayground": {
+ "title": "Песочница JSONPath",
+ "subtitle": "Запрашивайте документы JSON с помощью JSONPath или JMESPath и смотрите результаты вживую",
+ "engineLabel": "Движок",
+ "queryLabel": "Запрос",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Шпаргалка",
+ "cheats": {
+ "jpAllTitles": "Все названия книг",
+ "jpAllAuthors": "Все авторы в любом месте документа",
+ "jpFirstBook": "Первая книга",
+ "jpLastBook": "Последняя книга",
+ "jpSlice": "Первые две книги (срез)",
+ "jpFilterPrice": "Книги дешевле 40",
+ "jpFilterInStock": "Названия книг в наличии",
+ "jpRecursive": "Все значения рекурсивно",
+ "jmAllTitles": "Все названия книг",
+ "jmFirstAuthor": "Автор первой книги",
+ "jmLastBook": "Последняя книга",
+ "jmSlice": "Первые две книги (срез)",
+ "jmFilterPrice": "Названия книг дешевле 40",
+ "jmCount": "Подсчёт книг в наличии",
+ "jmProjection": "Преобразовать каждую книгу в новый объект",
+ "jmSort": "Названия, отсортированные по цене"
+ },
+ "panels": {
+ "input": "Ввод JSON",
+ "results": "Результаты"
+ },
+ "sample": "Пример",
+ "format": "Формат",
+ "clear": "Очистить",
+ "formatError": "Не удаётся отформатировать: ввод не является корректным JSON",
+ "placeholder": "Вставьте или введите JSON здесь…",
+ "outputPlaceholder": "Совпадения появятся здесь…",
+ "matchCount": "{count, plural, one {# совпадение} few {# совпадения} many {# совпадений} other {# совпадения}}",
+ "copy": "Копировать",
+ "copied": "Скопировано"
+ },
+ "KeycodeInspector": {
+ "title": "Инспектор клавиш",
+ "subtitle": "Нажмите любую клавишу, чтобы изучить её событие клавиатуры JavaScript",
+ "capture": {
+ "prompt": "Нажмите любую клавишу…",
+ "ready": "Слушаю — нажмите любую клавишу на клавиатуре",
+ "clickToFocus": "Нажмите здесь для фокуса, затем нажмите любую клавишу"
+ },
+ "hero": {
+ "spaceKey": "(Пробел)",
+ "keyLabel": "event.key",
+ "repeat": "повтор"
+ },
+ "copyJson": "Копировать JSON",
+ "copiedJson": "JSON события скопирован в буфер обмена",
+ "deprecated": "устарело",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Стандартная",
+ "left": "Слева",
+ "right": "Справа",
+ "numpad": "Цифровой блок",
+ "unknown": "Неизвестно"
+ },
+ "history": {
+ "title": "История",
+ "clear": "Очистить историю",
+ "empty": "Нажатия клавиш появятся здесь."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Генератор таблиц Markdown",
+ "subtitle": "Редактируйте сетку, вставляйте из таблицы и копируйте красиво выровненную таблицу Markdown или HTML",
+ "addRow": "Добавить строку",
+ "addColumn": "Добавить столбец",
+ "importLabel": "Импорт",
+ "importPlaceholder": "Вставьте TSV (из Excel / Google Sheets) или CSV сюда…",
+ "importButton": "Импортировать в сетку",
+ "pasteClipboard": "Вставить из буфера обмена",
+ "importEmpty": "Нечего импортировать — сначала вставьте несколько строк",
+ "clipboardError": "Не удалось прочитать из буфера обмена",
+ "clear": "Очистить",
+ "panels": {
+ "editor": "Редактор таблицы",
+ "output": "Результат"
+ },
+ "align": {
+ "left": "Выровнять по левому краю",
+ "center": "Выровнять по центру",
+ "right": "Выровнять по правому краю"
+ },
+ "headerPlaceholder": "Заголовок",
+ "cellPlaceholder": "Ячейка",
+ "removeRow": "Удалить строку",
+ "removeColumn": "Удалить столбец",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Копировать",
+ "copied": "Скопировано"
+ },
+ "TimezoneConverter": {
+ "title": "Конвертер часовых поясов",
+ "subtitle": "Преобразуйте один момент по всему миру для планирования встреч.",
+ "dateTimeLabel": "Дата и время",
+ "now": "Сейчас",
+ "sourceZoneLabel": "Исходный часовой пояс",
+ "selectZone": "Выберите пояс",
+ "searchZone": "Поиск часовых поясов...",
+ "noZone": "Часовой пояс не найден.",
+ "scrubLabel": "Сдвинуть время",
+ "reset": "Сбросить",
+ "targetsLabel": "Целевые пояса",
+ "addZone": "Добавить пояс",
+ "remove": "Удалить",
+ "localBadge": "Локальный",
+ "invalidDate": "Введите корректные дату и время.",
+ "bands": {
+ "work": "Рабочие часы (09:00–18:00)",
+ "edge": "Крайние часы (раннее утро / вечер)",
+ "night": "Ночь"
+ }
+ },
+ "TokenCounter": {
+ "title": "Счётчик токенов LLM",
+ "subtitle": "Считайте токены, визуализируйте чанки и оценивайте стоимость API.",
+ "inputLabel": "Текст",
+ "sample": "Пример",
+ "clear": "Очистить",
+ "placeholder": "Вставьте текст, код или JSON для подсчёта токенов...",
+ "encoding": "Кодировка",
+ "loading": "Загрузка токенизатора...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / серия o)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Токены",
+ "characters": "Символы",
+ "words": "Слова",
+ "ratio": "Токенов/слово",
+ "ratioSub": "прибл."
+ },
+ "claudeRow": "Claude (приблизительно)",
+ "estimate": "оценка",
+ "viz": {
+ "title": "Визуализация токенов",
+ "toggle": "Показать",
+ "hint": "Включите, чтобы раскрасить каждый токен как чанк.",
+ "empty": "Введите текст для визуализации токенов.",
+ "truncated": "Показаны первые {limit} из {total} токенов."
+ },
+ "cost": {
+ "title": "Калькулятор стоимости",
+ "disclaimer": "редактируемые значения по умолчанию — проверьте актуальные цены",
+ "model": "Модель",
+ "expectedOutput": "Ожидаемые токены вывода",
+ "customInput": "Ввод $/MTok",
+ "customOutput": "Вывод $/MTok",
+ "inputCost": "Стоимость ввода",
+ "outputCost": "Стоимость вывода",
+ "total": "Итого",
+ "per1000": "За 1000 вызовов"
+ }
+ },
+ "WebhookTester": {
+ "title": "Тестер вебхуков",
+ "subtitle": "Создайте временный URL и изучайте входящие запросы вебхуков вживую.",
+ "intro": {
+ "title": "Создайте URL вебхука, чтобы начать захват запросов",
+ "hint": "Вы получите непредсказуемый URL, который записывает каждый входящий запрос. Он истекает через 24 часа."
+ },
+ "createBtn": "Создать URL вебхука",
+ "creating": "Создание…",
+ "urlLabel": "Ваш URL вебхука",
+ "expiresIn": "Истекает через {countdown}",
+ "copyUrlBtn": "Копировать URL",
+ "copied": "Скопировано",
+ "clearBtn": "Очистить",
+ "newBinBtn": "Новый URL",
+ "requestsTitle": "Запросы",
+ "selectPrompt": "Выберите запрос, чтобы изучить его детали.",
+ "expiredNotice": "Этот URL вебхука истёк. Создайте новый, чтобы продолжить тестирование.",
+ "base64Badge": "base64",
+ "truncatedBadge": "обрезано",
+ "empty": {
+ "title": "Ожидание запросов…",
+ "hint": "Отправьте запрос на ваш URL, и он появится здесь мгновенно. Попробуйте так:"
+ },
+ "detail": {
+ "receivedAt": "Получено",
+ "source": "IP источника",
+ "contentType": "Content-Type",
+ "query": "Параметры query",
+ "headers": "Заголовки",
+ "body": "Тело",
+ "copyBody": "Копировать тело",
+ "emptyBody": "Нет тела"
+ },
+ "errors": {
+ "createFailed": "Не удалось создать URL вебхука. Попробуйте снова.",
+ "clearFailed": "Не удалось очистить запросы. Попробуйте снова."
+ }
+ },
+ "WebsocketTester": {
+ "title": "Тестер WebSocket",
+ "subtitle": "Подключайтесь к эндпоинту WebSocket и отправляйте, получайте и изучайте сообщения вживую",
+ "urlLabel": "URL эндпоинта",
+ "protocolsLabel": "Подпротоколы",
+ "protocolsPlaceholder": "необязательно, через запятую",
+ "connect": "Подключить",
+ "disconnect": "Отключить",
+ "status": {
+ "disconnected": "Отключено",
+ "connecting": "Подключение",
+ "connected": "Подключено",
+ "error": "Ошибка"
+ },
+ "messages": {
+ "connecting": "Подключение к {url}…",
+ "connected": "Соединение установлено",
+ "connectedWithProtocol": "Соединение установлено (протокол: {protocol})",
+ "invalidUrl": "Некорректный URL — должен начинаться с ws:// или wss://",
+ "connectFailed": "Не удалось открыть соединение",
+ "socketError": "Произошла ошибка соединения",
+ "notConnected": "Не подключено — сначала подключитесь",
+ "binaryMessage": "Получено двоичное сообщение"
+ },
+ "log": {
+ "title": "Журнал сообщений",
+ "empty": "Сообщений пока нет. Подключитесь и начните отправлять.",
+ "copyAll": "Копировать всё",
+ "download": "Скачать как .txt",
+ "clear": "Очистить журнал",
+ "prettyPrint": "Форматировать JSON",
+ "showRaw": "Показать необработанный текст"
+ },
+ "composer": {
+ "placeholder": "Введите сообщение для отправки…",
+ "send": "Отправить",
+ "formatJson": "Форматировать как JSON",
+ "invalidJson": "Сообщение не является корректным JSON",
+ "hint": "Cmd/Ctrl+Enter для отправки"
+ },
+ "ping": {
+ "label": "Авто-пинг",
+ "seconds": "с",
+ "intervalTitle": "Интервал пинга в секундах",
+ "payloadPlaceholder": "payload пинга"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Whois-запрос",
+ "subtitle": "Узнавайте данные регистрации любого домена или IP-адреса через RDAP",
+ "placeholder": "example.com или 8.8.8.8",
+ "lookupBtn": "Запросить",
+ "looking": "Запрос…",
+ "refreshBtn": "Обновить",
+ "tryLabel": "Попробуйте:",
+ "copy": "Копировать",
+ "copied": "Скопировано",
+ "notAvailable": "Недоступно",
+ "summary": {
+ "registrar": "Регистратор",
+ "network": "Сеть"
+ },
+ "dates": {
+ "created": "Создан",
+ "updated": "Обновлён",
+ "expires": "Истекает"
+ },
+ "sections": {
+ "status": "Статус",
+ "nameservers": "Серверы имён",
+ "contacts": "Контакты",
+ "rawRdap": "Необработанный RDAP"
+ },
+ "contacts": {
+ "roles": "Роли",
+ "name": "Имя",
+ "organization": "Организация"
+ },
+ "errors": {
+ "invalidTarget": "Введите корректное доменное имя или IP-адрес",
+ "lookupFailed": "Запрос не удался (статус {status})"
+ }
}
}
diff --git a/apps/web/messages/sv.json b/apps/web/messages/sv.json
index 95872ae6..dc78bc92 100644
--- a/apps/web/messages/sv.json
+++ b/apps/web/messages/sv.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Chmod-kalkylator",
+ "codeScreenshot": "Kodskärmbild",
+ "cssGenerators": "CSS-generatorer",
+ "exifViewer": "EXIF-visare",
+ "faviconGenerator": "Favicon-generator",
+ "jsonDiff": "JSON-diff",
+ "jsonpathPlayground": "JSONPath-lekplats",
+ "keycodeInspector": "Tangentkodsinspektör",
+ "markdownTableGenerator": "Markdown-tabell",
+ "timezoneConverter": "Tidszonskonverterare",
+ "tokenCounter": "Token-räknare",
+ "webhookTester": "Webhook-testare",
+ "websocketTester": "WebSocket-testare",
+ "whoisLookup": "Whois-uppslag"
},
"Help": {
"title": "Hjälp och dokumentation",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Bcrypt-generator och -kontroll",
+ "description": "Hasha lösenord med bcrypt och verifiera befintliga hashar."
+ },
+ "chmodCalculator": {
+ "title": "Chmod-kalkylator",
+ "description": "Konvertera Unix-behörigheter mellan oktal, symbolisk notation och kryssrutor."
+ },
+ "codeScreenshot": {
+ "title": "Kodskärmbild",
+ "description": "Förvandla källkod till vackra, delbara bilder."
+ },
+ "cssGenerators": {
+ "title": "CSS-generatorer",
+ "description": "Generatorer för box-shadow, border-radius och flytande clamp()."
+ },
+ "exifViewer": {
+ "title": "EXIF-visare och -borttagare",
+ "description": "Inspektera och ta bort EXIF-metadata från foton lokalt."
+ },
+ "faviconGenerator": {
+ "title": "Favicon-generator",
+ "description": "Generera favicons, appikoner och manifest-utdrag."
+ },
+ "jsonDiff": {
+ "title": "JSON-diff",
+ "description": "Semantisk, strukturmedveten jämförelse av två JSON-dokument."
+ },
+ "jsonpathPlayground": {
+ "title": "JSONPath-lekplats",
+ "description": "Fråga JSON med JSONPath eller JMESPath och se resultat i realtid."
+ },
+ "keycodeInspector": {
+ "title": "Tangentkodsinspektör",
+ "description": "Tryck på en tangent för att se dess JavaScript-händelsekoder och modifierare."
+ },
+ "markdownTableGenerator": {
+ "title": "Markdown-tabellgenerator",
+ "description": "Bygg Markdown- och HTML-tabeller i en kalkylbladsliknande rutnätsredigerare."
+ },
+ "timezoneConverter": {
+ "title": "Tidszonskonverterare",
+ "description": "Konvertera en tid över många zoner med dagsförskjutning och markeringar för arbetstid."
+ },
+ "tokenCounter": {
+ "title": "LLM-token-räknare",
+ "description": "Räkna LLM-tokens, visualisera chunkar och uppskatta API-kostnad."
+ },
+ "webhookTester": {
+ "title": "Webhook-testare",
+ "description": "Fånga och inspektera inkommande webhook-förfrågningar i realtid."
+ },
+ "websocketTester": {
+ "title": "WebSocket-testare",
+ "description": "Anslut till WebSocket-slutpunkter och utbyt meddelanden i realtid."
+ },
+ "whoisLookup": {
+ "title": "Whois-uppslag",
+ "description": "Slå upp registreringsuppgifter för domäner och IP via RDAP."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Bcrypt-generator och -kontroll",
+ "subtitle": "Hasha lösenord med bcrypt och verifiera hashar — allt stannar i din webbläsare",
+ "tabs": {
+ "hash": "Hasha",
+ "verify": "Verifiera"
+ },
+ "showPassword": "Visa lösenord",
+ "hidePassword": "Dölj lösenord",
+ "copy": "Kopiera",
+ "copied": "Kopierat",
+ "hash": {
+ "passwordLabel": "Lösenord",
+ "passwordPlaceholder": "Ange ett lösenord att hasha…",
+ "costLabel": "Kostnadsfaktor",
+ "costHint": "Högre kostnad fördubblar arbetet per steg. 10–12 är ett vanligt val för produktion.",
+ "slowWarning": "Kostnadsfaktorer på 13 och högre kan ta flera sekunder att beräkna.",
+ "generate": "Generera hash",
+ "generating": "Hashar…",
+ "resultLabel": "Bcrypt-hash",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "Hash-anatomi",
+ "algorithm": "algoritm",
+ "cost": "kostnad",
+ "salt": "salt (22 tecken)",
+ "hash": "hash (31 tecken)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Lösenord",
+ "passwordPlaceholder": "Ange lösenordet att kontrollera…",
+ "hashLabel": "Bcrypt-hash",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Ange ett lösenord och en bcrypt-hash för att jämföra dem.",
+ "invalidHash": "Detta ser inte ut som en giltig bcrypt-hash.",
+ "checking": "Jämför…",
+ "match": "Matchar — lösenordet ger denna hash.",
+ "noMatch": "Ingen matchning — lösenordet ger inte denna hash."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Chmod-kalkylator",
+ "subtitle": "Konvertera Unix-filbehörigheter mellan kryssrutor, oktal och symbolisk notation",
+ "grid": {
+ "title": "Behörigheter",
+ "read": "Läs",
+ "write": "Skriv",
+ "execute": "Kör",
+ "owner": "Ägare",
+ "group": "Grupp",
+ "others": "Andra"
+ },
+ "special": {
+ "title": "Specialbitar",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "Oktal",
+ "placeholder": "755 eller 4755",
+ "invalid": "Ange 3 eller 4 oktala siffror (0–7)."
+ },
+ "symbolic": {
+ "label": "Symbolisk",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Ange 9 tecken som rwxr-xr-x (s/S, t/T tillåtna)."
+ },
+ "output": {
+ "title": "Kommando",
+ "copy": "Kopiera",
+ "copied": "Kopierat"
+ },
+ "explain": {
+ "line": "{cls} kan {perms}.",
+ "none": "{cls} har inga behörigheter.",
+ "setuid": "Setuid: filen körs med ägarens behörigheter.",
+ "setgid": "Setgid: filen körs med gruppens behörigheter (nya filer i en katalog ärver dess grupp).",
+ "sticky": "Sticky: endast en fils ägare kan radera eller byta namn på den i denna katalog."
+ },
+ "presets": {
+ "title": "Vanliga förinställningar",
+ "octal": "Oktal",
+ "permissions": "Behörigheter",
+ "description": "Typisk användning",
+ "p644": "Vanliga filer: ägaren skriver, alla läser.",
+ "p755": "Körbara filer och kataloger: alla kan läsa och gå in.",
+ "p600": "Privata filer: endast ägaren läser och skriver.",
+ "p700": "Privata körbara filer och kataloger: endast ägaren.",
+ "p664": "Grupp-skrivbara filer.",
+ "p775": "Grupp-skrivbara kataloger och körbara filer.",
+ "p400": "Skrivskyddad för ägaren (t.ex. privata SSH-nycklar)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Kodskärmbild",
+ "subtitle": "Förvandla kod till vackra, delbara bilder",
+ "languageLabel": "Språk",
+ "autoDetect": "Automatisk identifiering",
+ "backgroundLabel": "Bakgrund",
+ "paddingLabel": "Utfyllnad",
+ "filenameLabel": "Filnamn",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "Fönsterprickar",
+ "themeLabel": "Fönstertema",
+ "themeDark": "Mörkt",
+ "themeLight": "Ljust",
+ "scaleLabel": "Exportskala",
+ "copyImage": "Kopiera bild",
+ "copied": "Kopierat",
+ "exportPng": "Exportera PNG",
+ "inputLabel": "Kod",
+ "detected": "Identifierat: {language}",
+ "clear": "Rensa",
+ "placeholder": "Klistra in din kod här…",
+ "previewLabel": "Förhandsgranskning",
+ "emptyPreview": "Förhandsgranskningen av din kod visas här",
+ "exportFailed": "Kunde inte exportera bild",
+ "copyUnsupported": "Kopiering av bilder stöds inte i den här webbläsaren",
+ "copyFailed": "Kunde inte kopiera bild",
+ "backgrounds": {
+ "ocean": "Ocean",
+ "sunset": "Solnedgång",
+ "candy": "Godis",
+ "forest": "Skog",
+ "midnight": "Midnatt",
+ "gold": "Guld",
+ "aurora": "Norrsken",
+ "slate": "Skiffer",
+ "transparent": "Transparent"
+ }
+ },
+ "CssGenerators": {
+ "title": "CSS-generatorer",
+ "subtitle": "Box-shadow, border-radius och flytande clamp() — kopiera CSS-koden.",
+ "preview": "Förhandsgranskning",
+ "output": "CSS",
+ "copy": "Kopiera",
+ "copied": "Kopierat",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "Flytande clamp()"
+ },
+ "boxShadow": {
+ "presets": "Förinställningar",
+ "layer": "Lager",
+ "addLayer": "Lägg till lager",
+ "removeLayer": "Ta bort lager",
+ "x": "X",
+ "y": "Y",
+ "blur": "Oskärpa",
+ "spread": "Spridning",
+ "color": "Färg",
+ "alpha": "Alfa",
+ "inset": "Inre",
+ "presetNames": {
+ "subtleCard": "Diskret kort",
+ "elevated": "Upphöjd",
+ "hardOffset": "Hård förskjutning",
+ "neonGlow": "Neonglöd",
+ "inner": "Inre",
+ "layeredSmooth": "Lagrad mjuk"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Läge",
+ "modes": {
+ "simple": "Per hörn",
+ "blob": "Organisk klick"
+ },
+ "unit": "Enhet",
+ "linkCorners": "Länka hörn",
+ "randomize": "Slumpa",
+ "horizontal": "Horisontella radier",
+ "vertical": "Vertikala radier",
+ "corners": {
+ "tl": "Uppe till vänster",
+ "tr": "Uppe till höger",
+ "br": "Nere till höger",
+ "bl": "Nere till vänster"
+ }
+ },
+ "clamp": {
+ "minFont": "Min. typsnitt (px)",
+ "maxFont": "Max. typsnitt (px)",
+ "minViewport": "Min. viewport (px)",
+ "maxViewport": "Max. viewport (px)",
+ "rootFont": "Rot-typsnitt (px)",
+ "viewportWidth": "Simulerad viewport",
+ "computedSize": "Beräknad storlek",
+ "slope": "Lutning",
+ "intercept": "Skärningspunkt",
+ "previewText": "Flygande bäckasiner söka",
+ "invalid": "Ange giltiga, olika viewport-gränser."
+ }
+ },
+ "ExifViewer": {
+ "title": "EXIF-visare och -borttagare",
+ "subtitle": "Inspektera fotons metadata och ta bort dem i din webbläsare",
+ "dropTitle": "Släpp en bild här",
+ "dropHint": "eller klicka för att bläddra — din bild lämnar aldrig din enhet",
+ "browse": "Välj bild",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Läser metadata…",
+ "parseFailed": "Kunde inte läsa den här bilden",
+ "clear": "Ta bort bild",
+ "gpsWarning": "Innehåller GPS-plats",
+ "openMap": "Visa på OpenStreetMap",
+ "noMetadata": "Inga inbäddade metadata hittades i den här bilden.",
+ "facts": {
+ "name": "Namn",
+ "type": "Typ",
+ "dimensions": "Dimensioner",
+ "size": "Storlek"
+ },
+ "groups": {
+ "camera": "Kamera",
+ "exposure": "Exponering",
+ "timestamps": "Tidsstämplar",
+ "gps": "GPS-plats",
+ "software": "Programvara och övrigt"
+ },
+ "gpsCard": {
+ "latitude": "Latitud",
+ "longitude": "Longitud",
+ "altitude": "Höjd"
+ },
+ "raw": {
+ "title": "Alla metadata",
+ "copyJson": "Kopiera JSON",
+ "copied": "Kopierat"
+ },
+ "remove": {
+ "title": "Ta bort metadata",
+ "jpegDescription": "Ta bort EXIF, XMP, IPTC och kommentarer förlustfritt utan att komprimera om bilden.",
+ "reencodeDescription": "Koda om bilden för att ta bort inbäddade metadata. Detta komprimerar om pixlarna.",
+ "keepIcc": "Behåll färgprofil (ICC)",
+ "stripButton": "Ta bort metadata",
+ "reencodeButton": "Koda om och ta bort",
+ "unsupported": "Borttagning av metadata stöds inte för den här filtypen.",
+ "failed": "Kunde inte ta bort metadata",
+ "before": "Före",
+ "after": "Efter",
+ "download": "Ladda ner rensad bild"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Favicon-generator",
+ "subtitle": "Skapa favicons och appikoner från en enda bild",
+ "dropTitle": "Släpp en bild här",
+ "dropHint": "PNG, JPEG eller SVG — klicka för att bläddra",
+ "browse": "Välj bild",
+ "clear": "Ta bort bild",
+ "backgroundLabel": "Bakgrund",
+ "transparent": "Transparent",
+ "solidColor": "Enfärgad",
+ "cornerLabel": "Hörnform",
+ "cornerNone": "Fyrkantig",
+ "cornerRounded": "Rundad",
+ "cornerCircle": "Cirkel",
+ "maskPaddingLabel": "Maskerbar utfyllnad",
+ "maskable": "maskable",
+ "previewLabel": "Förhandsgranskning",
+ "copy": "Kopiera",
+ "headSnippet": "HTML head-utdrag",
+ "downloadIco": "Ladda ner favicon.ico",
+ "downloadZip": "Ladda ner alla (.zip)"
+ },
+ "JsonDiff": {
+ "title": "JSON-diff",
+ "subtitle": "Semantisk, strukturmedveten jämförelse av två JSON-dokument — nyckelordning och formatering ignoreras",
+ "options": {
+ "ignoreArrayOrder": "Ignorera array-ordning"
+ },
+ "loadBothSamples": "Ladda exempel",
+ "sample": "Exempel",
+ "format": "Formatera",
+ "clear": "Rensa",
+ "formatError": "Kan inte formatera: indata är inte giltig JSON",
+ "placeholder": "Klistra in eller skriv JSON här…",
+ "panels": {
+ "left": "Original (vänster)",
+ "right": "Ändrad (höger)",
+ "results": "Skillnader"
+ },
+ "summary": {
+ "added": "{count} tillagda",
+ "removed": "{count} borttagna",
+ "changed": "{count} ändrade"
+ },
+ "types": {
+ "added": "Tillagd",
+ "removed": "Borttagen",
+ "changed": "Ändrad"
+ },
+ "copyChanges": "Kopiera som JSON",
+ "copied": "Kopierat",
+ "empty": "Klistra in JSON i båda panelerna för att se skillnaderna.",
+ "identical": "De två dokumenten är semantiskt identiska."
+ },
+ "JsonpathPlayground": {
+ "title": "JSONPath-lekplats",
+ "subtitle": "Fråga JSON-dokument med JSONPath eller JMESPath och se resultat i realtid",
+ "engineLabel": "Motor",
+ "queryLabel": "Fråga",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Fusklapp",
+ "cheats": {
+ "jpAllTitles": "Alla boktitlar",
+ "jpAllAuthors": "Varje författare, var som helst i dokumentet",
+ "jpFirstBook": "Första boken",
+ "jpLastBook": "Sista boken",
+ "jpSlice": "De två första böckerna (utsnitt)",
+ "jpFilterPrice": "Böcker billigare än 40",
+ "jpFilterInStock": "Titlar på böcker i lager",
+ "jpRecursive": "Alla värden, rekursivt",
+ "jmAllTitles": "Alla boktitlar",
+ "jmFirstAuthor": "Författare till den första boken",
+ "jmLastBook": "Sista boken",
+ "jmSlice": "De två första böckerna (utsnitt)",
+ "jmFilterPrice": "Titlar på böcker billigare än 40",
+ "jmCount": "Antal böcker i lager",
+ "jmProjection": "Omforma varje bok till ett nytt objekt",
+ "jmSort": "Titlar sorterade efter pris"
+ },
+ "panels": {
+ "input": "JSON-indata",
+ "results": "Resultat"
+ },
+ "sample": "Exempel",
+ "format": "Formatera",
+ "clear": "Rensa",
+ "formatError": "Kan inte formatera: indata är inte giltig JSON",
+ "placeholder": "Klistra in eller skriv JSON här…",
+ "outputPlaceholder": "Matchningar visas här…",
+ "matchCount": "{count, plural, one {# matchning} other {# matchningar}}",
+ "copy": "Kopiera",
+ "copied": "Kopierat"
+ },
+ "KeycodeInspector": {
+ "title": "Tangentkodsinspektör",
+ "subtitle": "Tryck på valfri tangent för att inspektera dess JavaScript-tangentbordshändelse",
+ "capture": {
+ "prompt": "Tryck på valfri tangent…",
+ "ready": "Lyssnar — tryck på valfri tangent på tangentbordet",
+ "clickToFocus": "Klicka här för att fokusera, tryck sedan på valfri tangent"
+ },
+ "hero": {
+ "spaceKey": "(Mellanslag)",
+ "keyLabel": "event.key",
+ "repeat": "upprepning"
+ },
+ "copyJson": "Kopiera JSON",
+ "copiedJson": "Händelse-JSON kopierad till urklipp",
+ "deprecated": "föråldrad",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Standard",
+ "left": "Vänster",
+ "right": "Höger",
+ "numpad": "Numeriskt tangentbord",
+ "unknown": "Okänd"
+ },
+ "history": {
+ "title": "Historik",
+ "clear": "Rensa historik",
+ "empty": "Tangenttryckningar visas här."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Markdown-tabellgenerator",
+ "subtitle": "Redigera ett rutnät, klistra in från ett kalkylblad och kopiera en snyggt justerad Markdown- eller HTML-tabell",
+ "addRow": "Lägg till rad",
+ "addColumn": "Lägg till kolumn",
+ "importLabel": "Importera",
+ "importPlaceholder": "Klistra in TSV (från Excel / Google Kalkylark) eller CSV här…",
+ "importButton": "Importera till rutnät",
+ "pasteClipboard": "Klistra in från urklipp",
+ "importEmpty": "Inget att importera — klistra in några rader först",
+ "clipboardError": "Kunde inte läsa från urklipp",
+ "clear": "Rensa",
+ "panels": {
+ "editor": "Tabellredigerare",
+ "output": "Utdata"
+ },
+ "align": {
+ "left": "Vänsterjustera",
+ "center": "Centrera",
+ "right": "Högerjustera"
+ },
+ "headerPlaceholder": "Rubrik",
+ "cellPlaceholder": "Cell",
+ "removeRow": "Ta bort rad",
+ "removeColumn": "Ta bort kolumn",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Kopiera",
+ "copied": "Kopierat"
+ },
+ "TimezoneConverter": {
+ "title": "Tidszonskonverterare",
+ "subtitle": "Konvertera ett ögonblick över hela världen för mötesplanering.",
+ "dateTimeLabel": "Datum och tid",
+ "now": "Nu",
+ "sourceZoneLabel": "Källtidszon",
+ "selectZone": "Välj zon",
+ "searchZone": "Sök tidszoner...",
+ "noZone": "Ingen tidszon hittades.",
+ "scrubLabel": "Förskjut tid",
+ "reset": "Återställ",
+ "targetsLabel": "Målzoner",
+ "addZone": "Lägg till zon",
+ "remove": "Ta bort",
+ "localBadge": "Lokal",
+ "invalidDate": "Ange ett giltigt datum och tid.",
+ "bands": {
+ "work": "Arbetstid (09:00–18:00)",
+ "edge": "Randtimmar (tidigt / kväll)",
+ "night": "Natt"
+ }
+ },
+ "TokenCounter": {
+ "title": "LLM-token-räknare",
+ "subtitle": "Räkna tokens, visualisera chunkar och uppskatta API-kostnad.",
+ "inputLabel": "Text",
+ "sample": "Exempel",
+ "clear": "Rensa",
+ "placeholder": "Klistra in text, kod eller JSON för att räkna tokens...",
+ "encoding": "Kodning",
+ "loading": "Laddar tokenizer...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / o-serien)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Tokens",
+ "characters": "Tecken",
+ "words": "Ord",
+ "ratio": "Tokens/ord",
+ "ratioSub": "ungefär"
+ },
+ "claudeRow": "Claude (ungefärligt)",
+ "estimate": "uppskattning",
+ "viz": {
+ "title": "Token-visualisering",
+ "toggle": "Visa",
+ "hint": "Växla för att färglägga varje token som en chunk.",
+ "empty": "Ange text för att visualisera tokens.",
+ "truncated": "Visar de första {limit} av {total} tokens."
+ },
+ "cost": {
+ "title": "Kostnadskalkylator",
+ "disclaimer": "redigerbara standardvärden — verifiera aktuella priser",
+ "model": "Modell",
+ "expectedOutput": "Förväntade output-tokens",
+ "customInput": "Input $/MTok",
+ "customOutput": "Output $/MTok",
+ "inputCost": "Input-kostnad",
+ "outputCost": "Output-kostnad",
+ "total": "Totalt",
+ "per1000": "Per 1000 anrop"
+ }
+ },
+ "WebhookTester": {
+ "title": "Webhook-testare",
+ "subtitle": "Skapa en tillfällig URL och inspektera inkommande webhook-förfrågningar live.",
+ "intro": {
+ "title": "Skapa en webhook-URL för att börja fånga förfrågningar",
+ "hint": "Du får en ogissningsbar URL som registrerar varje inkommande förfrågan. Den upphör efter 24 timmar."
+ },
+ "createBtn": "Skapa webhook-URL",
+ "creating": "Skapar…",
+ "urlLabel": "Din webhook-URL",
+ "expiresIn": "Upphör om {countdown}",
+ "copyUrlBtn": "Kopiera URL",
+ "copied": "Kopierat",
+ "clearBtn": "Rensa",
+ "newBinBtn": "Ny URL",
+ "requestsTitle": "Förfrågningar",
+ "selectPrompt": "Välj en förfrågan för att inspektera dess detaljer.",
+ "expiredNotice": "Den här webhook-URL:en har upphört. Skapa en ny för att fortsätta testa.",
+ "base64Badge": "base64",
+ "truncatedBadge": "avkortad",
+ "empty": {
+ "title": "Väntar på förfrågningar…",
+ "hint": "Skicka en förfrågan till din URL så visas den här direkt. Prova detta:"
+ },
+ "detail": {
+ "receivedAt": "Mottagen",
+ "source": "Käll-IP",
+ "contentType": "Content-Type",
+ "query": "Frågeparametrar",
+ "headers": "Headers",
+ "body": "Body",
+ "copyBody": "Kopiera body",
+ "emptyBody": "Ingen body"
+ },
+ "errors": {
+ "createFailed": "Kunde inte skapa webhook-URL. Försök igen.",
+ "clearFailed": "Kunde inte rensa förfrågningar. Försök igen."
+ }
+ },
+ "WebsocketTester": {
+ "title": "WebSocket-testare",
+ "subtitle": "Anslut till en WebSocket-slutpunkt och skicka, ta emot och inspektera meddelanden live",
+ "urlLabel": "Slutpunkts-URL",
+ "protocolsLabel": "Underprotokoll",
+ "protocolsPlaceholder": "valfritt, kommaseparerat",
+ "connect": "Anslut",
+ "disconnect": "Koppla från",
+ "status": {
+ "disconnected": "Frånkopplad",
+ "connecting": "Ansluter",
+ "connected": "Ansluten",
+ "error": "Fel"
+ },
+ "messages": {
+ "connecting": "Ansluter till {url}…",
+ "connected": "Anslutning upprättad",
+ "connectedWithProtocol": "Anslutning upprättad (protokoll: {protocol})",
+ "invalidUrl": "Ogiltig URL — måste börja med ws:// eller wss://",
+ "connectFailed": "Kunde inte öppna anslutningen",
+ "socketError": "Ett anslutningsfel uppstod",
+ "notConnected": "Inte ansluten — anslut först",
+ "binaryMessage": "Tog emot ett binärt meddelande"
+ },
+ "log": {
+ "title": "Meddelandelogg",
+ "empty": "Inga meddelanden ännu. Anslut och börja skicka.",
+ "copyAll": "Kopiera alla",
+ "download": "Ladda ner som .txt",
+ "clear": "Rensa logg",
+ "prettyPrint": "Formatera JSON snyggt",
+ "showRaw": "Visa rå text"
+ },
+ "composer": {
+ "placeholder": "Skriv ett meddelande att skicka…",
+ "send": "Skicka",
+ "formatJson": "Formatera som JSON",
+ "invalidJson": "Meddelandet är inte giltig JSON",
+ "hint": "Cmd/Ctrl+Enter för att skicka"
+ },
+ "ping": {
+ "label": "Auto-ping",
+ "seconds": "sek",
+ "intervalTitle": "Ping-intervall i sekunder",
+ "payloadPlaceholder": "ping-payload"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Whois-uppslag",
+ "subtitle": "Slå upp registreringsuppgifter för valfri domän eller IP-adress via RDAP",
+ "placeholder": "example.com eller 8.8.8.8",
+ "lookupBtn": "Slå upp",
+ "looking": "Slår upp…",
+ "refreshBtn": "Uppdatera",
+ "tryLabel": "Prova:",
+ "copy": "Kopiera",
+ "copied": "Kopierat",
+ "notAvailable": "Ej tillgänglig",
+ "summary": {
+ "registrar": "Registrator",
+ "network": "Nätverk"
+ },
+ "dates": {
+ "created": "Skapad",
+ "updated": "Uppdaterad",
+ "expires": "Upphör"
+ },
+ "sections": {
+ "status": "Status",
+ "nameservers": "Namnservrar",
+ "contacts": "Kontakter",
+ "rawRdap": "Rå RDAP"
+ },
+ "contacts": {
+ "roles": "Roller",
+ "name": "Namn",
+ "organization": "Organisation"
+ },
+ "errors": {
+ "invalidTarget": "Ange ett giltigt domännamn eller IP-adress",
+ "lookupFailed": "Uppslaget misslyckades (status {status})"
+ }
}
}
diff --git a/apps/web/messages/tr.json b/apps/web/messages/tr.json
index 1d11e7a3..5a1b1c27 100644
--- a/apps/web/messages/tr.json
+++ b/apps/web/messages/tr.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Chmod Hesaplayıcı",
+ "codeScreenshot": "Kod Ekran Görüntüsü",
+ "cssGenerators": "CSS Üreticileri",
+ "exifViewer": "EXIF Görüntüleyici",
+ "faviconGenerator": "Favicon Üreticisi",
+ "jsonDiff": "JSON Karşılaştırma",
+ "jsonpathPlayground": "JSONPath Oyun Alanı",
+ "keycodeInspector": "Tuş Kodu İnceleyici",
+ "markdownTableGenerator": "Markdown Tablosu",
+ "timezoneConverter": "Saat Dilimi Dönüştürücü",
+ "tokenCounter": "Token Sayacı",
+ "webhookTester": "Webhook Test Aracı",
+ "websocketTester": "WebSocket Test Aracı",
+ "whoisLookup": "Whois Sorgulama"
},
"Help": {
"title": "Yardım ve dokümantasyon",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Bcrypt Üretici ve Doğrulayıcı",
+ "description": "Parolaları bcrypt ile hash'leyin ve mevcut hash'leri doğrulayın."
+ },
+ "chmodCalculator": {
+ "title": "Chmod Hesaplayıcı",
+ "description": "Unix izinlerini sekizlik, sembolik ve onay kutuları arasında dönüştürün."
+ },
+ "codeScreenshot": {
+ "title": "Kod Ekran Görüntüsü",
+ "description": "Kaynak kodu güzel ve paylaşılabilir görsellere dönüştürün."
+ },
+ "cssGenerators": {
+ "title": "CSS Üreticileri",
+ "description": "box-shadow, border-radius ve akışkan clamp() üreticileri."
+ },
+ "exifViewer": {
+ "title": "EXIF Görüntüleyici ve Temizleyici",
+ "description": "Fotoğraf EXIF meta verilerini yerel olarak inceleyin ve temizleyin."
+ },
+ "faviconGenerator": {
+ "title": "Favicon Üreticisi",
+ "description": "Favicon, uygulama simgeleri ve manifest parçacıkları oluşturun."
+ },
+ "jsonDiff": {
+ "title": "JSON Karşılaştırma",
+ "description": "İki JSON belgesinin anlamsal, yapı farkında karşılaştırması."
+ },
+ "jsonpathPlayground": {
+ "title": "JSONPath Oyun Alanı",
+ "description": "JSON'ı JSONPath veya JMESPath ile sorgulayın ve sonuçları canlı görün."
+ },
+ "keycodeInspector": {
+ "title": "Tuş Kodu İnceleyici",
+ "description": "Bir tuşa basarak JavaScript olay kodlarını ve değiştiricileri görün."
+ },
+ "markdownTableGenerator": {
+ "title": "Markdown Tablosu Üreticisi",
+ "description": "Markdown ve HTML tablolarını elektronik tablo benzeri bir ızgara düzenleyicide oluşturun."
+ },
+ "timezoneConverter": {
+ "title": "Saat Dilimi Dönüştürücü",
+ "description": "Bir zamanı birçok dilime; gün kayması ve çalışma saati ipuçlarıyla dönüştürün."
+ },
+ "tokenCounter": {
+ "title": "LLM Token Sayacı",
+ "description": "LLM token'larını sayın, parçaları görselleştirin ve API maliyetini tahmin edin."
+ },
+ "webhookTester": {
+ "title": "Webhook Test Aracı",
+ "description": "Gelen webhook isteklerini gerçek zamanlı yakalayın ve inceleyin."
+ },
+ "websocketTester": {
+ "title": "WebSocket Test Aracı",
+ "description": "WebSocket uç noktalarına bağlanın ve mesajları gerçek zamanlı alışverişini yapın."
+ },
+ "whoisLookup": {
+ "title": "Whois Sorgulama",
+ "description": "Alan adı ve IP kayıt bilgilerini RDAP üzerinden sorgulayın."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Bcrypt Üretici ve Doğrulayıcı",
+ "subtitle": "Parolaları bcrypt ile hash'leyin ve hash'leri doğrulayın — her şey tarayıcınızda kalır",
+ "tabs": {
+ "hash": "Hash'le",
+ "verify": "Doğrula"
+ },
+ "showPassword": "Parolayı göster",
+ "hidePassword": "Parolayı gizle",
+ "copy": "Kopyala",
+ "copied": "Kopyalandı",
+ "hash": {
+ "passwordLabel": "Parola",
+ "passwordPlaceholder": "Hash'lenecek bir parola girin…",
+ "costLabel": "Maliyet faktörü",
+ "costHint": "Daha yüksek maliyet, her adımdaki işi ikiye katlar. Üretim için genellikle 10-12 tercih edilir.",
+ "slowWarning": "13 ve üzeri maliyet faktörlerinin hesaplanması birkaç saniye sürebilir.",
+ "generate": "Hash oluştur",
+ "generating": "Hash'leniyor…",
+ "resultLabel": "Bcrypt hash'i",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "Hash anatomisi",
+ "algorithm": "algoritma",
+ "cost": "maliyet",
+ "salt": "salt (22 karakter)",
+ "hash": "hash (31 karakter)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Parola",
+ "passwordPlaceholder": "Kontrol edilecek parolayı girin…",
+ "hashLabel": "Bcrypt hash'i",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Karşılaştırmak için bir parola ve bir bcrypt hash'i girin.",
+ "invalidHash": "Bu, geçerli bir bcrypt hash'i gibi görünmüyor.",
+ "checking": "Karşılaştırılıyor…",
+ "match": "Eşleşti — parola bu hash'i üretiyor.",
+ "noMatch": "Eşleşmedi — parola bu hash'i üretmiyor."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Chmod Hesaplayıcı",
+ "subtitle": "Unix dosya izinlerini onay kutuları, sekizlik ve sembolik gösterim arasında dönüştürün",
+ "grid": {
+ "title": "İzinler",
+ "read": "Okuma",
+ "write": "Yazma",
+ "execute": "Çalıştırma",
+ "owner": "Sahip",
+ "group": "Grup",
+ "others": "Diğerleri"
+ },
+ "special": {
+ "title": "Özel bitler",
+ "setuid": "setuid",
+ "setgid": "setgid",
+ "sticky": "sticky"
+ },
+ "octal": {
+ "label": "Sekizlik",
+ "placeholder": "755 veya 4755",
+ "invalid": "3 veya 4 sekizlik basamak girin (0-7)."
+ },
+ "symbolic": {
+ "label": "Sembolik",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "rwxr-xr-x gibi 9 karakter girin (s/S, t/T kabul edilir)."
+ },
+ "output": {
+ "title": "Komut",
+ "copy": "Kopyala",
+ "copied": "Kopyalandı"
+ },
+ "explain": {
+ "line": "{cls} şunları yapabilir: {perms}.",
+ "none": "{cls} hiçbir izne sahip değil.",
+ "setuid": "setuid: dosya, sahibinin ayrıcalıklarıyla çalışır.",
+ "setgid": "setgid: dosya, grubunun ayrıcalıklarıyla çalışır (bir dizindeki yeni dosyalar onun grubunu devralır).",
+ "sticky": "sticky: bu dizin içindeki bir dosyayı yalnızca sahibi silebilir veya yeniden adlandırabilir."
+ },
+ "presets": {
+ "title": "Yaygın ön ayarlar",
+ "octal": "Sekizlik",
+ "permissions": "İzinler",
+ "description": "Tipik kullanım",
+ "p644": "Normal dosyalar: sahibi yazar, herkes okur.",
+ "p755": "Çalıştırılabilirler ve dizinler: herkes okuyabilir ve girebilir.",
+ "p600": "Özel dosyalar: yalnızca sahibi okur ve yazar.",
+ "p700": "Özel çalıştırılabilirler ve dizinler: yalnızca sahibi.",
+ "p664": "Grup tarafından yazılabilen dosyalar.",
+ "p775": "Grup tarafından yazılabilen dizinler ve çalıştırılabilirler.",
+ "p400": "Sahibi için salt okunur (ör. SSH özel anahtarları)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Kod Ekran Görüntüsü",
+ "subtitle": "Kodu güzel, paylaşılabilir görsellere dönüştürün",
+ "languageLabel": "Dil",
+ "autoDetect": "Otomatik algıla",
+ "backgroundLabel": "Arka plan",
+ "paddingLabel": "İç boşluk",
+ "filenameLabel": "Dosya adı",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "Pencere noktaları",
+ "themeLabel": "Pencere teması",
+ "themeDark": "Koyu",
+ "themeLight": "Açık",
+ "scaleLabel": "Dışa aktarma ölçeği",
+ "copyImage": "Görseli kopyala",
+ "copied": "Kopyalandı",
+ "exportPng": "PNG dışa aktar",
+ "inputLabel": "Kod",
+ "detected": "Algılanan: {language}",
+ "clear": "Temizle",
+ "placeholder": "Kodunuzu buraya yapıştırın…",
+ "previewLabel": "Önizleme",
+ "emptyPreview": "Kod önizlemeniz burada görünecek",
+ "exportFailed": "Görsel dışa aktarılamadı",
+ "copyUnsupported": "Görsel kopyalama bu tarayıcıda desteklenmiyor",
+ "copyFailed": "Görsel kopyalanamadı",
+ "backgrounds": {
+ "ocean": "Okyanus",
+ "sunset": "Gün batımı",
+ "candy": "Şeker",
+ "forest": "Orman",
+ "midnight": "Gece yarısı",
+ "gold": "Altın",
+ "aurora": "Kutup ışığı",
+ "slate": "Arduvaz",
+ "transparent": "Şeffaf"
+ }
+ },
+ "CssGenerators": {
+ "title": "CSS Üreticileri",
+ "subtitle": "box-shadow, border-radius ve akışkan clamp() — CSS'i kopyalayın.",
+ "preview": "Önizleme",
+ "output": "CSS",
+ "copy": "Kopyala",
+ "copied": "Kopyalandı",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "Akışkan clamp()"
+ },
+ "boxShadow": {
+ "presets": "Ön ayarlar",
+ "layer": "Katman",
+ "addLayer": "Katman ekle",
+ "removeLayer": "Katmanı kaldır",
+ "x": "X",
+ "y": "Y",
+ "blur": "Bulanıklık",
+ "spread": "Yayılım",
+ "color": "Renk",
+ "alpha": "Alfa",
+ "inset": "İç gölge",
+ "presetNames": {
+ "subtleCard": "İnce kart",
+ "elevated": "Yükseltilmiş",
+ "hardOffset": "Sert kaydırma",
+ "neonGlow": "Neon parıltı",
+ "inner": "İç",
+ "layeredSmooth": "Katmanlı yumuşak"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Mod",
+ "modes": {
+ "simple": "Köşe başına",
+ "blob": "Organik blob"
+ },
+ "unit": "Birim",
+ "linkCorners": "Köşeleri bağla",
+ "randomize": "Rastgele",
+ "horizontal": "Yatay yarıçaplar",
+ "vertical": "Dikey yarıçaplar",
+ "corners": {
+ "tl": "Sol üst",
+ "tr": "Sağ üst",
+ "br": "Sağ alt",
+ "bl": "Sol alt"
+ }
+ },
+ "clamp": {
+ "minFont": "Min yazı tipi (px)",
+ "maxFont": "Maks yazı tipi (px)",
+ "minViewport": "Min viewport (px)",
+ "maxViewport": "Maks viewport (px)",
+ "rootFont": "Kök yazı tipi (px)",
+ "viewportWidth": "Simüle edilen viewport",
+ "computedSize": "Hesaplanan boyut",
+ "slope": "Eğim",
+ "intercept": "Kesişim",
+ "previewText": "Önizleme için örnek metin",
+ "invalid": "Geçerli ve birbirinden farklı viewport sınırları girin."
+ }
+ },
+ "ExifViewer": {
+ "title": "EXIF Görüntüleyici ve Temizleyici",
+ "subtitle": "Fotoğraf meta verilerini inceleyin ve tarayıcınızda temizleyin",
+ "dropTitle": "Bir görseli buraya bırakın",
+ "dropHint": "veya göz atmak için tıklayın — görseliniz asla cihazınızdan çıkmaz",
+ "browse": "Görsel seç",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Meta veri okunuyor…",
+ "parseFailed": "Bu görsel okunamadı",
+ "clear": "Görseli kaldır",
+ "gpsWarning": "GPS konumu içeriyor",
+ "openMap": "OpenStreetMap'te görüntüle",
+ "noMetadata": "Bu görselde gömülü meta veri bulunamadı.",
+ "facts": {
+ "name": "Ad",
+ "type": "Tür",
+ "dimensions": "Boyutlar",
+ "size": "Boyut"
+ },
+ "groups": {
+ "camera": "Kamera",
+ "exposure": "Pozlama",
+ "timestamps": "Zaman damgaları",
+ "gps": "GPS Konumu",
+ "software": "Yazılım ve Diğer"
+ },
+ "gpsCard": {
+ "latitude": "Enlem",
+ "longitude": "Boylam",
+ "altitude": "Rakım"
+ },
+ "raw": {
+ "title": "Tüm meta veriler",
+ "copyJson": "JSON kopyala",
+ "copied": "Kopyalandı"
+ },
+ "remove": {
+ "title": "Meta veriyi kaldır",
+ "jpegDescription": "Görseli yeniden sıkıştırmadan EXIF, XMP, IPTC ve yorumları kayıpsız kaldırın.",
+ "reencodeDescription": "Gömülü meta veriyi temizlemek için görseli yeniden kodlayın. Bu, pikselleri yeniden sıkıştırır.",
+ "keepIcc": "Renk profilini koru (ICC)",
+ "stripButton": "Meta veriyi temizle",
+ "reencodeButton": "Yeniden kodla ve temizle",
+ "unsupported": "Bu dosya türü için meta veri kaldırma desteklenmiyor.",
+ "failed": "Meta veri kaldırılamadı",
+ "before": "Önce",
+ "after": "Sonra",
+ "download": "Temizlenmiş görseli indir"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Favicon Üreticisi",
+ "subtitle": "Tek bir görselden favicon ve uygulama simgeleri oluşturun",
+ "dropTitle": "Bir görseli buraya bırakın",
+ "dropHint": "PNG, JPEG veya SVG — göz atmak için tıklayın",
+ "browse": "Görsel seç",
+ "clear": "Görseli kaldır",
+ "backgroundLabel": "Arka plan",
+ "transparent": "Şeffaf",
+ "solidColor": "Düz renk",
+ "cornerLabel": "Köşe şekli",
+ "cornerNone": "Kare",
+ "cornerRounded": "Yuvarlatılmış",
+ "cornerCircle": "Daire",
+ "maskPaddingLabel": "Maskelenebilir iç boşluk",
+ "maskable": "maskable",
+ "previewLabel": "Önizleme",
+ "copy": "Kopyala",
+ "headSnippet": "HTML head parçacığı",
+ "downloadIco": "favicon.ico indir",
+ "downloadZip": "Tümünü indir (.zip)"
+ },
+ "JsonDiff": {
+ "title": "JSON Karşılaştırma",
+ "subtitle": "İki JSON belgesinin anlamsal, yapı farkında karşılaştırması — anahtar sırası ve biçimlendirme yok sayılır",
+ "options": {
+ "ignoreArrayOrder": "Dizi sırasını yok say"
+ },
+ "loadBothSamples": "Örnekleri yükle",
+ "sample": "Örnek",
+ "format": "Biçimlendir",
+ "clear": "Temizle",
+ "formatError": "Biçimlendirilemiyor: giriş geçerli JSON değil",
+ "placeholder": "JSON'ı buraya yapıştırın veya yazın…",
+ "panels": {
+ "left": "Orijinal (sol)",
+ "right": "Değiştirilmiş (sağ)",
+ "results": "Farklar"
+ },
+ "summary": {
+ "added": "{count} eklendi",
+ "removed": "{count} kaldırıldı",
+ "changed": "{count} değişti"
+ },
+ "types": {
+ "added": "Eklendi",
+ "removed": "Kaldırıldı",
+ "changed": "Değişti"
+ },
+ "copyChanges": "JSON olarak kopyala",
+ "copied": "Kopyalandı",
+ "empty": "Farklarını görmek için her iki panele de JSON yapıştırın.",
+ "identical": "İki belge anlamsal olarak aynı."
+ },
+ "JsonpathPlayground": {
+ "title": "JSONPath Oyun Alanı",
+ "subtitle": "JSON belgelerini JSONPath veya JMESPath ile sorgulayın ve sonuçları canlı görün",
+ "engineLabel": "Motor",
+ "queryLabel": "Sorgu",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Kopya kâğıdı",
+ "cheats": {
+ "jpAllTitles": "Tüm kitap başlıkları",
+ "jpAllAuthors": "Belgenin herhangi bir yerindeki her yazar",
+ "jpFirstBook": "İlk kitap",
+ "jpLastBook": "Son kitap",
+ "jpSlice": "İlk iki kitap (dilim)",
+ "jpFilterPrice": "40'tan ucuz kitaplar",
+ "jpFilterInStock": "Stoktaki kitapların başlıkları",
+ "jpRecursive": "Tüm değerler, özyinelemeli",
+ "jmAllTitles": "Tüm kitap başlıkları",
+ "jmFirstAuthor": "İlk kitabın yazarı",
+ "jmLastBook": "Son kitap",
+ "jmSlice": "İlk iki kitap (dilim)",
+ "jmFilterPrice": "40'tan ucuz kitapların başlıkları",
+ "jmCount": "Stoktaki kitapları say",
+ "jmProjection": "Her kitabı yeni bir nesneye dönüştür",
+ "jmSort": "Fiyata göre sıralanmış başlıklar"
+ },
+ "panels": {
+ "input": "JSON girişi",
+ "results": "Sonuçlar"
+ },
+ "sample": "Örnek",
+ "format": "Biçimlendir",
+ "clear": "Temizle",
+ "formatError": "Biçimlendirilemiyor: giriş geçerli JSON değil",
+ "placeholder": "JSON'ı buraya yapıştırın veya yazın…",
+ "outputPlaceholder": "Eşleşmeler burada görünecek…",
+ "matchCount": "{count, plural, one {# eşleşme} other {# eşleşme}}",
+ "copy": "Kopyala",
+ "copied": "Kopyalandı"
+ },
+ "KeycodeInspector": {
+ "title": "Tuş Kodu İnceleyici",
+ "subtitle": "JavaScript klavye olayını incelemek için herhangi bir tuşa basın",
+ "capture": {
+ "prompt": "Herhangi bir tuşa basın…",
+ "ready": "Dinleniyor — klavyenizde herhangi bir tuşa basın",
+ "clickToFocus": "Odaklanmak için buraya tıklayın, sonra herhangi bir tuşa basın"
+ },
+ "hero": {
+ "spaceKey": "(Boşluk)",
+ "keyLabel": "event.key",
+ "repeat": "yineleme"
+ },
+ "copyJson": "JSON kopyala",
+ "copiedJson": "Olay JSON'ı panoya kopyalandı",
+ "deprecated": "kullanımdan kaldırıldı",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Standart",
+ "left": "Sol",
+ "right": "Sağ",
+ "numpad": "Sayı tuş takımı",
+ "unknown": "Bilinmiyor"
+ },
+ "history": {
+ "title": "Geçmiş",
+ "clear": "Geçmişi temizle",
+ "empty": "Tuş basmaları burada görünecek."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Markdown Tablosu Üreticisi",
+ "subtitle": "Bir ızgarayı düzenleyin, elektronik tablodan yapıştırın ve düzgün hizalanmış bir Markdown veya HTML tablosu kopyalayın",
+ "addRow": "Satır ekle",
+ "addColumn": "Sütun ekle",
+ "importLabel": "İçe aktar",
+ "importPlaceholder": "TSV (Excel / Google Sheets'ten) veya CSV'yi buraya yapıştırın…",
+ "importButton": "Izgaraya içe aktar",
+ "pasteClipboard": "Panodan yapıştır",
+ "importEmpty": "İçe aktarılacak bir şey yok — önce birkaç satır yapıştırın",
+ "clipboardError": "Pano okunamadı",
+ "clear": "Temizle",
+ "panels": {
+ "editor": "Tablo düzenleyici",
+ "output": "Çıktı"
+ },
+ "align": {
+ "left": "Sola hizala",
+ "center": "Ortala",
+ "right": "Sağa hizala"
+ },
+ "headerPlaceholder": "Başlık",
+ "cellPlaceholder": "Hücre",
+ "removeRow": "Satırı kaldır",
+ "removeColumn": "Sütunu kaldır",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Kopyala",
+ "copied": "Kopyalandı"
+ },
+ "TimezoneConverter": {
+ "title": "Saat Dilimi Dönüştürücü",
+ "subtitle": "Toplantı planlaması için bir anı dünya genelinde dönüştürün.",
+ "dateTimeLabel": "Tarih ve saat",
+ "now": "Şimdi",
+ "sourceZoneLabel": "Kaynak saat dilimi",
+ "selectZone": "Dilim seç",
+ "searchZone": "Saat dilimlerinde ara...",
+ "noZone": "Saat dilimi bulunamadı.",
+ "scrubLabel": "Zamanı kaydır",
+ "reset": "Sıfırla",
+ "targetsLabel": "Hedef dilimler",
+ "addZone": "Dilim ekle",
+ "remove": "Kaldır",
+ "localBadge": "Yerel",
+ "invalidDate": "Geçerli bir tarih ve saat girin.",
+ "bands": {
+ "work": "Çalışma saatleri (09:00–18:00)",
+ "edge": "Sınır saatleri (erken / akşam)",
+ "night": "Gece"
+ }
+ },
+ "TokenCounter": {
+ "title": "LLM Token Sayacı",
+ "subtitle": "Token'ları sayın, parçaları görselleştirin ve API maliyetini tahmin edin.",
+ "inputLabel": "Metin",
+ "sample": "Örnek",
+ "clear": "Temizle",
+ "placeholder": "Token saymak için metin, kod veya JSON yapıştırın...",
+ "encoding": "Kodlama",
+ "loading": "Tokenizer yükleniyor...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / o-series)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Token",
+ "characters": "Karakter",
+ "words": "Kelime",
+ "ratio": "Token/kelime",
+ "ratioSub": "yaklaşık"
+ },
+ "claudeRow": "Claude (yaklaşık)",
+ "estimate": "tahmin",
+ "viz": {
+ "title": "Token görselleştirme",
+ "toggle": "Göster",
+ "hint": "Her token'ı bir parça olarak renklendirmek için değiştirin.",
+ "empty": "Token'ları görselleştirmek için metin girin.",
+ "truncated": "{total} token'ın ilk {limit} tanesi gösteriliyor."
+ },
+ "cost": {
+ "title": "Maliyet hesaplayıcı",
+ "disclaimer": "düzenlenebilir varsayılanlar — güncel fiyatlandırmayı doğrulayın",
+ "model": "Model",
+ "expectedOutput": "Beklenen çıktı token'ları",
+ "customInput": "Giriş $/MTok",
+ "customOutput": "Çıkış $/MTok",
+ "inputCost": "Giriş maliyeti",
+ "outputCost": "Çıkış maliyeti",
+ "total": "Toplam",
+ "per1000": "1000 çağrı başına"
+ }
+ },
+ "WebhookTester": {
+ "title": "Webhook Test Aracı",
+ "subtitle": "Geçici bir URL oluşturun ve gelen webhook isteklerini canlı inceleyin.",
+ "intro": {
+ "title": "İstekleri yakalamaya başlamak için bir webhook URL'si oluşturun",
+ "hint": "Her gelen isteği kaydeden, tahmin edilemez bir URL alacaksınız. 24 saat sonra süresi dolar."
+ },
+ "createBtn": "Webhook URL'si oluştur",
+ "creating": "Oluşturuluyor…",
+ "urlLabel": "Webhook URL'niz",
+ "expiresIn": "Süre bitişi: {countdown}",
+ "copyUrlBtn": "URL kopyala",
+ "copied": "Kopyalandı",
+ "clearBtn": "Temizle",
+ "newBinBtn": "Yeni URL",
+ "requestsTitle": "İstekler",
+ "selectPrompt": "Ayrıntılarını incelemek için bir istek seçin.",
+ "expiredNotice": "Bu webhook URL'sinin süresi doldu. Test etmeye devam etmek için yeni bir tane oluşturun.",
+ "base64Badge": "base64",
+ "truncatedBadge": "kırpıldı",
+ "empty": {
+ "title": "İstekler bekleniyor…",
+ "hint": "URL'nize bir istek gönderin ve anında burada görünsün. Bunu deneyin:"
+ },
+ "detail": {
+ "receivedAt": "Alındı",
+ "source": "Kaynak IP",
+ "contentType": "Content-Type",
+ "query": "Sorgu parametreleri",
+ "headers": "Başlıklar",
+ "body": "Gövde",
+ "copyBody": "Gövdeyi kopyala",
+ "emptyBody": "Gövde yok"
+ },
+ "errors": {
+ "createFailed": "Webhook URL'si oluşturulamadı. Lütfen tekrar deneyin.",
+ "clearFailed": "İstekler temizlenemedi. Lütfen tekrar deneyin."
+ }
+ },
+ "WebsocketTester": {
+ "title": "WebSocket Test Aracı",
+ "subtitle": "Bir WebSocket uç noktasına bağlanın ve mesajları canlı gönderin, alın ve inceleyin",
+ "urlLabel": "Uç nokta URL'si",
+ "protocolsLabel": "Alt protokoller",
+ "protocolsPlaceholder": "isteğe bağlı, virgülle ayrılmış",
+ "connect": "Bağlan",
+ "disconnect": "Bağlantıyı kes",
+ "status": {
+ "disconnected": "Bağlı değil",
+ "connecting": "Bağlanıyor",
+ "connected": "Bağlandı",
+ "error": "Hata"
+ },
+ "messages": {
+ "connecting": "{url} adresine bağlanılıyor…",
+ "connected": "Bağlantı kuruldu",
+ "connectedWithProtocol": "Bağlantı kuruldu (protokol: {protocol})",
+ "invalidUrl": "Geçersiz URL — ws:// veya wss:// ile başlamalı",
+ "connectFailed": "Bağlantı açılamadı",
+ "socketError": "Bir bağlantı hatası oluştu",
+ "notConnected": "Bağlı değil — önce bağlanın",
+ "binaryMessage": "İkili bir mesaj alındı"
+ },
+ "log": {
+ "title": "Mesaj Günlüğü",
+ "empty": "Henüz mesaj yok. Bağlanın ve göndermeye başlayın.",
+ "copyAll": "Tümünü kopyala",
+ "download": ".txt olarak indir",
+ "clear": "Günlüğü temizle",
+ "prettyPrint": "JSON'ı biçimlendir",
+ "showRaw": "Ham metni göster"
+ },
+ "composer": {
+ "placeholder": "Gönderilecek bir mesaj yazın…",
+ "send": "Gönder",
+ "formatJson": "JSON olarak biçimlendir",
+ "invalidJson": "Mesaj geçerli JSON değil",
+ "hint": "Göndermek için Cmd/Ctrl+Enter"
+ },
+ "ping": {
+ "label": "Otomatik ping",
+ "seconds": "sn",
+ "intervalTitle": "Saniye cinsinden ping aralığı",
+ "payloadPlaceholder": "ping yükü"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Whois Sorgulama",
+ "subtitle": "Herhangi bir alan adı veya IP adresinin kayıt bilgilerini RDAP üzerinden sorgulayın",
+ "placeholder": "example.com veya 8.8.8.8",
+ "lookupBtn": "Sorgula",
+ "looking": "Sorgulanıyor…",
+ "refreshBtn": "Yenile",
+ "tryLabel": "Deneyin:",
+ "copy": "Kopyala",
+ "copied": "Kopyalandı",
+ "notAvailable": "Mevcut değil",
+ "summary": {
+ "registrar": "Kayıt kuruluşu",
+ "network": "Ağ"
+ },
+ "dates": {
+ "created": "Oluşturuldu",
+ "updated": "Güncellendi",
+ "expires": "Süre bitişi"
+ },
+ "sections": {
+ "status": "Durum",
+ "nameservers": "Ad sunucuları",
+ "contacts": "Kişiler",
+ "rawRdap": "Ham RDAP"
+ },
+ "contacts": {
+ "roles": "Roller",
+ "name": "Ad",
+ "organization": "Kuruluş"
+ },
+ "errors": {
+ "invalidTarget": "Geçerli bir alan adı veya IP adresi girin",
+ "lookupFailed": "Sorgulama başarısız (durum {status})"
+ }
}
}
diff --git a/apps/web/messages/uk.json b/apps/web/messages/uk.json
index 26aed165..e7333e7e 100644
--- a/apps/web/messages/uk.json
+++ b/apps/web/messages/uk.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Калькулятор Chmod",
+ "codeScreenshot": "Скриншот коду",
+ "cssGenerators": "Генератори CSS",
+ "exifViewer": "Перегляд EXIF",
+ "faviconGenerator": "Генератор favicon",
+ "jsonDiff": "Порівняння JSON",
+ "jsonpathPlayground": "Пісочниця JSONPath",
+ "keycodeInspector": "Інспектор клавіш",
+ "markdownTableGenerator": "Таблиця Markdown",
+ "timezoneConverter": "Конвертер часових поясів",
+ "tokenCounter": "Лічильник токенів",
+ "webhookTester": "Тестер вебхуків",
+ "websocketTester": "Тестер WebSocket",
+ "whoisLookup": "Whois-запит"
},
"Help": {
"title": "Довідка та документація",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Генератор і перевірка Bcrypt",
+ "description": "Хешуйте паролі за допомогою bcrypt і перевіряйте наявні хеші."
+ },
+ "chmodCalculator": {
+ "title": "Калькулятор Chmod",
+ "description": "Перетворюйте права Unix між вісімковою, символьною формою та прапорцями."
+ },
+ "codeScreenshot": {
+ "title": "Скриншот коду",
+ "description": "Перетворюйте вихідний код на красиві зображення для публікації."
+ },
+ "cssGenerators": {
+ "title": "Генератори CSS",
+ "description": "Генератори box-shadow, border-radius та адаптивного clamp()."
+ },
+ "exifViewer": {
+ "title": "Перегляд і видалення EXIF",
+ "description": "Переглядайте та видаляйте метадані EXIF з фото локально."
+ },
+ "faviconGenerator": {
+ "title": "Генератор favicon",
+ "description": "Створюйте favicon, іконки застосунків і фрагменти маніфесту."
+ },
+ "jsonDiff": {
+ "title": "Порівняння JSON",
+ "description": "Семантичне порівняння двох документів JSON з урахуванням структури."
+ },
+ "jsonpathPlayground": {
+ "title": "Пісочниця JSONPath",
+ "description": "Запитуйте JSON за допомогою JSONPath або JMESPath і дивіться результати наживо."
+ },
+ "keycodeInspector": {
+ "title": "Інспектор клавіш",
+ "description": "Натисніть клавішу, щоб побачити її коди події JavaScript і модифікатори."
+ },
+ "markdownTableGenerator": {
+ "title": "Генератор таблиць Markdown",
+ "description": "Створюйте таблиці Markdown та HTML у табличному редакторі."
+ },
+ "timezoneConverter": {
+ "title": "Конвертер часових поясів",
+ "description": "Перетворюйте час між поясами з підказками про зміну дня та робочі години."
+ },
+ "tokenCounter": {
+ "title": "Лічильник токенів LLM",
+ "description": "Рахуйте токени LLM, візуалізуйте чанки та оцінюйте вартість API."
+ },
+ "webhookTester": {
+ "title": "Тестер вебхуків",
+ "description": "Захоплюйте та досліджуйте вхідні запити вебхуків у реальному часі."
+ },
+ "websocketTester": {
+ "title": "Тестер WebSocket",
+ "description": "Підключайтеся до ендпоінтів WebSocket і обмінюйтеся повідомленнями в реальному часі."
+ },
+ "whoisLookup": {
+ "title": "Whois-запит",
+ "description": "Дізнавайтеся дані реєстрації доменів та IP через RDAP."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Генератор і перевірка Bcrypt",
+ "subtitle": "Хешуйте паролі за допомогою bcrypt і перевіряйте хеші — усе залишається у вашому браузері",
+ "tabs": {
+ "hash": "Хеш",
+ "verify": "Перевірка"
+ },
+ "showPassword": "Показати пароль",
+ "hidePassword": "Приховати пароль",
+ "copy": "Копіювати",
+ "copied": "Скопійовано",
+ "hash": {
+ "passwordLabel": "Пароль",
+ "passwordPlaceholder": "Введіть пароль для хешування…",
+ "costLabel": "Фактор вартості",
+ "costHint": "Вищий фактор подвоює роботу на кожному кроці. 10-12 — поширений вибір для продакшену.",
+ "slowWarning": "Фактори вартості 13 і вище можуть обчислюватися кілька секунд.",
+ "generate": "Створити хеш",
+ "generating": "Хешування…",
+ "resultLabel": "Хеш bcrypt",
+ "timeTaken": "{ms} мс",
+ "anatomy": {
+ "title": "Будова хешу",
+ "algorithm": "алгоритм",
+ "cost": "вартість",
+ "salt": "сіль (22 символи)",
+ "hash": "хеш (31 символ)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Пароль",
+ "passwordPlaceholder": "Введіть пароль для перевірки…",
+ "hashLabel": "Хеш bcrypt",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Введіть пароль і хеш bcrypt, щоб порівняти їх.",
+ "invalidHash": "Це не схоже на коректний хеш bcrypt.",
+ "checking": "Порівняння…",
+ "match": "Збігається — пароль дає цей хеш.",
+ "noMatch": "Не збігається — пароль не дає цей хеш."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Калькулятор Chmod",
+ "subtitle": "Перетворюйте права файлів Unix між прапорцями, вісімковою та символьною формою",
+ "grid": {
+ "title": "Права",
+ "read": "Читання",
+ "write": "Запис",
+ "execute": "Виконання",
+ "owner": "Власник",
+ "group": "Група",
+ "others": "Інші"
+ },
+ "special": {
+ "title": "Спеціальні біти",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "Вісімкова",
+ "placeholder": "755 або 4755",
+ "invalid": "Введіть 3 або 4 вісімкові цифри (0-7)."
+ },
+ "symbolic": {
+ "label": "Символьна",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Введіть 9 символів на кшталт rwxr-xr-x (дозволені s/S, t/T)."
+ },
+ "output": {
+ "title": "Команда",
+ "copy": "Копіювати",
+ "copied": "Скопійовано"
+ },
+ "explain": {
+ "line": "{cls} може {perms}.",
+ "none": "{cls} не має прав.",
+ "setuid": "Setuid: файл виконується з привілеями свого власника.",
+ "setgid": "Setgid: файл виконується з привілеями своєї групи (нові файли в каталозі успадковують його групу).",
+ "sticky": "Sticky: лише власник файлу може видалити або перейменувати його в цьому каталозі."
+ },
+ "presets": {
+ "title": "Поширені пресети",
+ "octal": "Вісімкова",
+ "permissions": "Права",
+ "description": "Типове застосування",
+ "p644": "Звичайні файли: власник пише, усі читають.",
+ "p755": "Виконувані файли та каталоги: усі можуть читати й заходити.",
+ "p600": "Приватні файли: лише власник читає й пише.",
+ "p700": "Приватні виконувані файли та каталоги: лише власник.",
+ "p664": "Файли, доступні групі для запису.",
+ "p775": "Каталоги та виконувані файли, доступні групі для запису.",
+ "p400": "Лише читання для власника (наприклад, приватні ключі SSH)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Скриншот коду",
+ "subtitle": "Перетворюйте код на красиві зображення для публікації",
+ "languageLabel": "Мова",
+ "autoDetect": "Автовизначення",
+ "backgroundLabel": "Фон",
+ "paddingLabel": "Відступ",
+ "filenameLabel": "Ім'я файлу",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "Крапки вікна",
+ "themeLabel": "Тема вікна",
+ "themeDark": "Темна",
+ "themeLight": "Світла",
+ "scaleLabel": "Масштаб експорту",
+ "copyImage": "Копіювати зображення",
+ "copied": "Скопійовано",
+ "exportPng": "Експорт PNG",
+ "inputLabel": "Код",
+ "detected": "Визначено: {language}",
+ "clear": "Очистити",
+ "placeholder": "Вставте ваш код сюди…",
+ "previewLabel": "Перегляд",
+ "emptyPreview": "Тут з'явиться перегляд вашого коду",
+ "exportFailed": "Не вдалося експортувати зображення",
+ "copyUnsupported": "Копіювання зображень не підтримується в цьому браузері",
+ "copyFailed": "Не вдалося скопіювати зображення",
+ "backgrounds": {
+ "ocean": "Океан",
+ "sunset": "Захід сонця",
+ "candy": "Цукерка",
+ "forest": "Ліс",
+ "midnight": "Північ",
+ "gold": "Золото",
+ "aurora": "Аврора",
+ "slate": "Сланець",
+ "transparent": "Прозорий"
+ }
+ },
+ "CssGenerators": {
+ "title": "Генератори CSS",
+ "subtitle": "Box-shadow, border-radius та адаптивний clamp() — копіюйте CSS.",
+ "preview": "Перегляд",
+ "output": "CSS",
+ "copy": "Копіювати",
+ "copied": "Скопійовано",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "Адаптивний clamp()"
+ },
+ "boxShadow": {
+ "presets": "Пресети",
+ "layer": "Шар",
+ "addLayer": "Додати шар",
+ "removeLayer": "Видалити шар",
+ "x": "X",
+ "y": "Y",
+ "blur": "Розмиття",
+ "spread": "Розтягнення",
+ "color": "Колір",
+ "alpha": "Альфа",
+ "inset": "Внутрішня",
+ "presetNames": {
+ "subtleCard": "Легка картка",
+ "elevated": "Піднята",
+ "hardOffset": "Жорстке зміщення",
+ "neonGlow": "Неонове сяйво",
+ "inner": "Внутрішня",
+ "layeredSmooth": "М'які шари"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Режим",
+ "modes": {
+ "simple": "По кутах",
+ "blob": "Органічна крапля"
+ },
+ "unit": "Одиниця",
+ "linkCorners": "Зв'язати кути",
+ "randomize": "Випадково",
+ "horizontal": "Горизонтальні радіуси",
+ "vertical": "Вертикальні радіуси",
+ "corners": {
+ "tl": "Верхній лівий",
+ "tr": "Верхній правий",
+ "br": "Нижній правий",
+ "bl": "Нижній лівий"
+ }
+ },
+ "clamp": {
+ "minFont": "Мін. шрифт (px)",
+ "maxFont": "Макс. шрифт (px)",
+ "minViewport": "Мін. viewport (px)",
+ "maxViewport": "Макс. viewport (px)",
+ "rootFont": "Кореневий шрифт (px)",
+ "viewportWidth": "Змодельований viewport",
+ "computedSize": "Обчислений розмір",
+ "slope": "Нахил",
+ "intercept": "Зміщення",
+ "previewText": "Швидка руда лисиця",
+ "invalid": "Введіть коректні та різні межі viewport."
+ }
+ },
+ "ExifViewer": {
+ "title": "Перегляд і видалення EXIF",
+ "subtitle": "Досліджуйте метадані фото та видаляйте їх у вашому браузері",
+ "dropTitle": "Перетягніть зображення сюди",
+ "dropHint": "або натисніть для вибору — ваше зображення ніколи не залишає ваш пристрій",
+ "browse": "Вибрати зображення",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Читання метаданих…",
+ "parseFailed": "Не вдалося прочитати це зображення",
+ "clear": "Видалити зображення",
+ "gpsWarning": "Містить GPS-координати",
+ "openMap": "Показати на OpenStreetMap",
+ "noMetadata": "У цьому зображенні не знайдено вбудованих метаданих.",
+ "facts": {
+ "name": "Ім'я",
+ "type": "Тип",
+ "dimensions": "Розміри",
+ "size": "Розмір"
+ },
+ "groups": {
+ "camera": "Камера",
+ "exposure": "Експозиція",
+ "timestamps": "Мітки часу",
+ "gps": "GPS-координати",
+ "software": "ПЗ та інше"
+ },
+ "gpsCard": {
+ "latitude": "Широта",
+ "longitude": "Довгота",
+ "altitude": "Висота"
+ },
+ "raw": {
+ "title": "Усі метадані",
+ "copyJson": "Копіювати JSON",
+ "copied": "Скопійовано"
+ },
+ "remove": {
+ "title": "Видалити метадані",
+ "jpegDescription": "Без втрат видаліть EXIF, XMP, IPTC і коментарі без повторного стиснення зображення.",
+ "reencodeDescription": "Перекодуйте зображення, щоб видалити вбудовані метадані. При цьому пікселі стискаються повторно.",
+ "keepIcc": "Зберегти колірний профіль (ICC)",
+ "stripButton": "Видалити метадані",
+ "reencodeButton": "Перекодувати й видалити",
+ "unsupported": "Видалення метаданих не підтримується для цього типу файлу.",
+ "failed": "Не вдалося видалити метадані",
+ "before": "До",
+ "after": "Після",
+ "download": "Завантажити очищене зображення"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Генератор favicon",
+ "subtitle": "Створюйте favicon та іконки застосунків з одного зображення",
+ "dropTitle": "Перетягніть зображення сюди",
+ "dropHint": "PNG, JPEG або SVG — натисніть для вибору",
+ "browse": "Вибрати зображення",
+ "clear": "Видалити зображення",
+ "backgroundLabel": "Фон",
+ "transparent": "Прозорий",
+ "solidColor": "Суцільний",
+ "cornerLabel": "Форма кутів",
+ "cornerNone": "Квадрат",
+ "cornerRounded": "Заокруглений",
+ "cornerCircle": "Коло",
+ "maskPaddingLabel": "Відступ maskable",
+ "maskable": "maskable",
+ "previewLabel": "Перегляд",
+ "copy": "Копіювати",
+ "headSnippet": "Фрагмент для head HTML",
+ "downloadIco": "Завантажити favicon.ico",
+ "downloadZip": "Завантажити все (.zip)"
+ },
+ "JsonDiff": {
+ "title": "Порівняння JSON",
+ "subtitle": "Семантичне порівняння двох документів JSON з урахуванням структури — порядок ключів і форматування ігноруються",
+ "options": {
+ "ignoreArrayOrder": "Ігнорувати порядок масивів"
+ },
+ "loadBothSamples": "Завантажити приклади",
+ "sample": "Приклад",
+ "format": "Формат",
+ "clear": "Очистити",
+ "formatError": "Не вдається відформатувати: введення не є коректним JSON",
+ "placeholder": "Вставте або введіть JSON тут…",
+ "panels": {
+ "left": "Оригінал (ліворуч)",
+ "right": "Змінений (праворуч)",
+ "results": "Відмінності"
+ },
+ "summary": {
+ "added": "{count} додано",
+ "removed": "{count} вилучено",
+ "changed": "{count} змінено"
+ },
+ "types": {
+ "added": "Додано",
+ "removed": "Вилучено",
+ "changed": "Змінено"
+ },
+ "copyChanges": "Копіювати як JSON",
+ "copied": "Скопійовано",
+ "empty": "Вставте JSON в обидві панелі, щоб побачити їхні відмінності.",
+ "identical": "Обидва документи семантично ідентичні."
+ },
+ "JsonpathPlayground": {
+ "title": "Пісочниця JSONPath",
+ "subtitle": "Запитуйте документи JSON за допомогою JSONPath або JMESPath і дивіться результати наживо",
+ "engineLabel": "Рушій",
+ "queryLabel": "Запит",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Шпаргалка",
+ "cheats": {
+ "jpAllTitles": "Усі назви книг",
+ "jpAllAuthors": "Усі автори будь-де в документі",
+ "jpFirstBook": "Перша книга",
+ "jpLastBook": "Остання книга",
+ "jpSlice": "Перші дві книги (зріз)",
+ "jpFilterPrice": "Книги дешевші за 40",
+ "jpFilterInStock": "Назви книг у наявності",
+ "jpRecursive": "Усі значення рекурсивно",
+ "jmAllTitles": "Усі назви книг",
+ "jmFirstAuthor": "Автор першої книги",
+ "jmLastBook": "Остання книга",
+ "jmSlice": "Перші дві книги (зріз)",
+ "jmFilterPrice": "Назви книг дешевших за 40",
+ "jmCount": "Підрахунок книг у наявності",
+ "jmProjection": "Перетворити кожну книгу на новий об'єкт",
+ "jmSort": "Назви, відсортовані за ціною"
+ },
+ "panels": {
+ "input": "Введення JSON",
+ "results": "Результати"
+ },
+ "sample": "Приклад",
+ "format": "Формат",
+ "clear": "Очистити",
+ "formatError": "Не вдається відформатувати: введення не є коректним JSON",
+ "placeholder": "Вставте або введіть JSON тут…",
+ "outputPlaceholder": "Збіги з'являться тут…",
+ "matchCount": "{count, plural, one {# збіг} few {# збіги} many {# збігів} other {# збігу}}",
+ "copy": "Копіювати",
+ "copied": "Скопійовано"
+ },
+ "KeycodeInspector": {
+ "title": "Інспектор клавіш",
+ "subtitle": "Натисніть будь-яку клавішу, щоб дослідити її подію клавіатури JavaScript",
+ "capture": {
+ "prompt": "Натисніть будь-яку клавішу…",
+ "ready": "Слухаю — натисніть будь-яку клавішу на клавіатурі",
+ "clickToFocus": "Натисніть тут для фокуса, потім натисніть будь-яку клавішу"
+ },
+ "hero": {
+ "spaceKey": "(Пробіл)",
+ "keyLabel": "event.key",
+ "repeat": "повтор"
+ },
+ "copyJson": "Копіювати JSON",
+ "copiedJson": "JSON події скопійовано до буфера обміну",
+ "deprecated": "застаріле",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Стандартне",
+ "left": "Ліворуч",
+ "right": "Праворуч",
+ "numpad": "Цифровий блок",
+ "unknown": "Невідоме"
+ },
+ "history": {
+ "title": "Історія",
+ "clear": "Очистити історію",
+ "empty": "Натискання клавіш з'являться тут."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Генератор таблиць Markdown",
+ "subtitle": "Редагуйте сітку, вставляйте з таблиці та копіюйте гарно вирівняну таблицю Markdown або HTML",
+ "addRow": "Додати рядок",
+ "addColumn": "Додати стовпець",
+ "importLabel": "Імпорт",
+ "importPlaceholder": "Вставте TSV (з Excel / Google Sheets) або CSV тут…",
+ "importButton": "Імпортувати в сітку",
+ "pasteClipboard": "Вставити з буфера обміну",
+ "importEmpty": "Немає що імпортувати — спершу вставте кілька рядків",
+ "clipboardError": "Не вдалося прочитати з буфера обміну",
+ "clear": "Очистити",
+ "panels": {
+ "editor": "Редактор таблиці",
+ "output": "Результат"
+ },
+ "align": {
+ "left": "Вирівняти ліворуч",
+ "center": "Вирівняти по центру",
+ "right": "Вирівняти праворуч"
+ },
+ "headerPlaceholder": "Заголовок",
+ "cellPlaceholder": "Комірка",
+ "removeRow": "Видалити рядок",
+ "removeColumn": "Видалити стовпець",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Копіювати",
+ "copied": "Скопійовано"
+ },
+ "TimezoneConverter": {
+ "title": "Конвертер часових поясів",
+ "subtitle": "Перетворюйте один момент по всьому світу для планування зустрічей.",
+ "dateTimeLabel": "Дата й час",
+ "now": "Зараз",
+ "sourceZoneLabel": "Вихідний часовий пояс",
+ "selectZone": "Виберіть пояс",
+ "searchZone": "Пошук часових поясів...",
+ "noZone": "Часовий пояс не знайдено.",
+ "scrubLabel": "Зсунути час",
+ "reset": "Скинути",
+ "targetsLabel": "Цільові пояси",
+ "addZone": "Додати пояс",
+ "remove": "Видалити",
+ "localBadge": "Локальний",
+ "invalidDate": "Введіть коректні дату й час.",
+ "bands": {
+ "work": "Робочі години (09:00–18:00)",
+ "edge": "Крайні години (ранній ранок / вечір)",
+ "night": "Ніч"
+ }
+ },
+ "TokenCounter": {
+ "title": "Лічильник токенів LLM",
+ "subtitle": "Рахуйте токени, візуалізуйте чанки та оцінюйте вартість API.",
+ "inputLabel": "Текст",
+ "sample": "Приклад",
+ "clear": "Очистити",
+ "placeholder": "Вставте текст, код або JSON для підрахунку токенів...",
+ "encoding": "Кодування",
+ "loading": "Завантаження токенізатора...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / серія o)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Токени",
+ "characters": "Символи",
+ "words": "Слова",
+ "ratio": "Токенів/слово",
+ "ratioSub": "прибл."
+ },
+ "claudeRow": "Claude (приблизно)",
+ "estimate": "оцінка",
+ "viz": {
+ "title": "Візуалізація токенів",
+ "toggle": "Показати",
+ "hint": "Увімкніть, щоб розфарбувати кожен токен як чанк.",
+ "empty": "Введіть текст для візуалізації токенів.",
+ "truncated": "Показано перші {limit} з {total} токенів."
+ },
+ "cost": {
+ "title": "Калькулятор вартості",
+ "disclaimer": "редаговані значення за замовчуванням — перевірте актуальні ціни",
+ "model": "Модель",
+ "expectedOutput": "Очікувані токени виводу",
+ "customInput": "Введення $/MTok",
+ "customOutput": "Вивід $/MTok",
+ "inputCost": "Вартість введення",
+ "outputCost": "Вартість виводу",
+ "total": "Разом",
+ "per1000": "За 1000 викликів"
+ }
+ },
+ "WebhookTester": {
+ "title": "Тестер вебхуків",
+ "subtitle": "Створіть тимчасовий URL і досліджуйте вхідні запити вебхуків наживо.",
+ "intro": {
+ "title": "Створіть URL вебхука, щоб почати захоплення запитів",
+ "hint": "Ви отримаєте непередбачуваний URL, який записує кожен вхідний запит. Він спливає через 24 години."
+ },
+ "createBtn": "Створити URL вебхука",
+ "creating": "Створення…",
+ "urlLabel": "Ваш URL вебхука",
+ "expiresIn": "Спливає через {countdown}",
+ "copyUrlBtn": "Копіювати URL",
+ "copied": "Скопійовано",
+ "clearBtn": "Очистити",
+ "newBinBtn": "Новий URL",
+ "requestsTitle": "Запити",
+ "selectPrompt": "Виберіть запит, щоб дослідити його деталі.",
+ "expiredNotice": "Цей URL вебхука сплив. Створіть новий, щоб продовжити тестування.",
+ "base64Badge": "base64",
+ "truncatedBadge": "обрізано",
+ "empty": {
+ "title": "Очікування запитів…",
+ "hint": "Надішліть запит на ваш URL, і він з'явиться тут миттєво. Спробуйте так:"
+ },
+ "detail": {
+ "receivedAt": "Отримано",
+ "source": "IP джерела",
+ "contentType": "Content-Type",
+ "query": "Параметри query",
+ "headers": "Заголовки",
+ "body": "Тіло",
+ "copyBody": "Копіювати тіло",
+ "emptyBody": "Немає тіла"
+ },
+ "errors": {
+ "createFailed": "Не вдалося створити URL вебхука. Спробуйте ще раз.",
+ "clearFailed": "Не вдалося очистити запити. Спробуйте ще раз."
+ }
+ },
+ "WebsocketTester": {
+ "title": "Тестер WebSocket",
+ "subtitle": "Підключайтеся до ендпоінта WebSocket і надсилайте, отримуйте та досліджуйте повідомлення наживо",
+ "urlLabel": "URL ендпоінта",
+ "protocolsLabel": "Підпротоколи",
+ "protocolsPlaceholder": "необов'язково, через кому",
+ "connect": "Підключити",
+ "disconnect": "Відключити",
+ "status": {
+ "disconnected": "Відключено",
+ "connecting": "Підключення",
+ "connected": "Підключено",
+ "error": "Помилка"
+ },
+ "messages": {
+ "connecting": "Підключення до {url}…",
+ "connected": "З'єднання встановлено",
+ "connectedWithProtocol": "З'єднання встановлено (протокол: {protocol})",
+ "invalidUrl": "Некоректний URL — має починатися з ws:// або wss://",
+ "connectFailed": "Не вдалося відкрити з'єднання",
+ "socketError": "Сталася помилка з'єднання",
+ "notConnected": "Не підключено — спершу підключіться",
+ "binaryMessage": "Отримано двійкове повідомлення"
+ },
+ "log": {
+ "title": "Журнал повідомлень",
+ "empty": "Повідомлень поки немає. Підключіться та почніть надсилати.",
+ "copyAll": "Копіювати все",
+ "download": "Завантажити як .txt",
+ "clear": "Очистити журнал",
+ "prettyPrint": "Форматувати JSON",
+ "showRaw": "Показати необроблений текст"
+ },
+ "composer": {
+ "placeholder": "Введіть повідомлення для надсилання…",
+ "send": "Надіслати",
+ "formatJson": "Форматувати як JSON",
+ "invalidJson": "Повідомлення не є коректним JSON",
+ "hint": "Cmd/Ctrl+Enter для надсилання"
+ },
+ "ping": {
+ "label": "Авто-пінг",
+ "seconds": "с",
+ "intervalTitle": "Інтервал пінгу в секундах",
+ "payloadPlaceholder": "payload пінгу"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Whois-запит",
+ "subtitle": "Дізнавайтеся дані реєстрації будь-якого домену або IP-адреси через RDAP",
+ "placeholder": "example.com або 8.8.8.8",
+ "lookupBtn": "Запитати",
+ "looking": "Запит…",
+ "refreshBtn": "Оновити",
+ "tryLabel": "Спробуйте:",
+ "copy": "Копіювати",
+ "copied": "Скопійовано",
+ "notAvailable": "Недоступно",
+ "summary": {
+ "registrar": "Реєстратор",
+ "network": "Мережа"
+ },
+ "dates": {
+ "created": "Створено",
+ "updated": "Оновлено",
+ "expires": "Спливає"
+ },
+ "sections": {
+ "status": "Статус",
+ "nameservers": "Сервери імен",
+ "contacts": "Контакти",
+ "rawRdap": "Необроблений RDAP"
+ },
+ "contacts": {
+ "roles": "Ролі",
+ "name": "Ім'я",
+ "organization": "Організація"
+ },
+ "errors": {
+ "invalidTarget": "Введіть коректне доменне ім'я або IP-адресу",
+ "lookupFailed": "Запит не вдався (статус {status})"
+ }
}
}
diff --git a/apps/web/messages/vi.json b/apps/web/messages/vi.json
index 9f332fdf..bfea25a5 100644
--- a/apps/web/messages/vi.json
+++ b/apps/web/messages/vi.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Máy tính Chmod",
+ "codeScreenshot": "Ảnh chụp mã",
+ "cssGenerators": "Trình tạo CSS",
+ "exifViewer": "Trình xem EXIF",
+ "faviconGenerator": "Trình tạo Favicon",
+ "jsonDiff": "So sánh JSON",
+ "jsonpathPlayground": "Sân chơi JSONPath",
+ "keycodeInspector": "Trình kiểm tra mã phím",
+ "markdownTableGenerator": "Bảng Markdown",
+ "timezoneConverter": "Chuyển đổi múi giờ",
+ "tokenCounter": "Bộ đếm token",
+ "webhookTester": "Trình kiểm tra Webhook",
+ "websocketTester": "Trình kiểm tra WebSocket",
+ "whoisLookup": "Tra cứu Whois"
},
"Help": {
"title": "Trợ giúp & tài liệu",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Trình tạo & kiểm tra Bcrypt",
+ "description": "Băm mật khẩu bằng bcrypt và xác minh các băm hiện có."
+ },
+ "chmodCalculator": {
+ "title": "Máy tính Chmod",
+ "description": "Chuyển đổi quyền Unix giữa bát phân, ký hiệu và hộp kiểm."
+ },
+ "codeScreenshot": {
+ "title": "Ảnh chụp mã",
+ "description": "Biến mã nguồn thành hình ảnh đẹp, dễ chia sẻ."
+ },
+ "cssGenerators": {
+ "title": "Trình tạo CSS",
+ "description": "Trình tạo box-shadow, border-radius và clamp() linh hoạt."
+ },
+ "exifViewer": {
+ "title": "Trình xem & xóa EXIF",
+ "description": "Kiểm tra và xóa metadata EXIF của ảnh ngay trên máy."
+ },
+ "faviconGenerator": {
+ "title": "Trình tạo Favicon",
+ "description": "Tạo favicon, biểu tượng ứng dụng và đoạn mã manifest."
+ },
+ "jsonDiff": {
+ "title": "So sánh JSON",
+ "description": "So sánh hai tài liệu JSON theo ngữ nghĩa và cấu trúc."
+ },
+ "jsonpathPlayground": {
+ "title": "Sân chơi JSONPath",
+ "description": "Truy vấn JSON bằng JSONPath hoặc JMESPath và xem kết quả trực tiếp."
+ },
+ "keycodeInspector": {
+ "title": "Trình kiểm tra mã phím",
+ "description": "Nhấn một phím để xem mã sự kiện JavaScript và các phím bổ trợ."
+ },
+ "markdownTableGenerator": {
+ "title": "Trình tạo bảng Markdown",
+ "description": "Tạo bảng Markdown và HTML trong trình chỉnh sửa dạng lưới như bảng tính."
+ },
+ "timezoneConverter": {
+ "title": "Chuyển đổi múi giờ",
+ "description": "Chuyển đổi một thời điểm qua nhiều múi giờ kèm gợi ý chênh lệch ngày và giờ làm việc."
+ },
+ "tokenCounter": {
+ "title": "Bộ đếm token LLM",
+ "description": "Đếm token LLM, trực quan hóa các khối và ước tính chi phí API."
+ },
+ "webhookTester": {
+ "title": "Trình kiểm tra Webhook",
+ "description": "Ghi lại và kiểm tra các yêu cầu webhook đến theo thời gian thực."
+ },
+ "websocketTester": {
+ "title": "Trình kiểm tra WebSocket",
+ "description": "Kết nối tới các điểm cuối WebSocket và trao đổi tin nhắn theo thời gian thực."
+ },
+ "whoisLookup": {
+ "title": "Tra cứu Whois",
+ "description": "Tra cứu thông tin đăng ký tên miền và IP qua RDAP."
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Trình tạo & kiểm tra Bcrypt",
+ "subtitle": "Băm mật khẩu bằng bcrypt và xác minh băm — mọi thứ diễn ra trong trình duyệt của bạn",
+ "tabs": {
+ "hash": "Băm",
+ "verify": "Xác minh"
+ },
+ "showPassword": "Hiện mật khẩu",
+ "hidePassword": "Ẩn mật khẩu",
+ "copy": "Sao chép",
+ "copied": "Đã sao chép",
+ "hash": {
+ "passwordLabel": "Mật khẩu",
+ "passwordPlaceholder": "Nhập mật khẩu cần băm…",
+ "costLabel": "Hệ số chi phí",
+ "costHint": "Chi phí càng cao thì khối lượng tính toán mỗi bước càng tăng gấp đôi. 10-12 là lựa chọn phổ biến cho môi trường sản xuất.",
+ "slowWarning": "Hệ số chi phí từ 13 trở lên có thể mất vài giây để tính toán.",
+ "generate": "Tạo băm",
+ "generating": "Đang băm…",
+ "resultLabel": "Băm Bcrypt",
+ "timeTaken": "{ms} ms",
+ "anatomy": {
+ "title": "Cấu trúc băm",
+ "algorithm": "thuật toán",
+ "cost": "chi phí",
+ "salt": "salt (22 ký tự)",
+ "hash": "băm (31 ký tự)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "Mật khẩu",
+ "passwordPlaceholder": "Nhập mật khẩu cần kiểm tra…",
+ "hashLabel": "Băm Bcrypt",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "Nhập mật khẩu và một băm bcrypt để so sánh chúng.",
+ "invalidHash": "Đây có vẻ không phải là một băm bcrypt hợp lệ.",
+ "checking": "Đang so sánh…",
+ "match": "Khớp — mật khẩu này tạo ra băm này.",
+ "noMatch": "Không khớp — mật khẩu này không tạo ra băm này."
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Máy tính Chmod",
+ "subtitle": "Chuyển đổi quyền tệp Unix giữa hộp kiểm, bát phân và ký hiệu tượng trưng",
+ "grid": {
+ "title": "Quyền",
+ "read": "Đọc",
+ "write": "Ghi",
+ "execute": "Thực thi",
+ "owner": "Chủ sở hữu",
+ "group": "Nhóm",
+ "others": "Người khác"
+ },
+ "special": {
+ "title": "Bit đặc biệt",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "Bát phân",
+ "placeholder": "755 hoặc 4755",
+ "invalid": "Nhập 3 hoặc 4 chữ số bát phân (0-7)."
+ },
+ "symbolic": {
+ "label": "Ký hiệu",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "Nhập 9 ký tự như rwxr-xr-x (cho phép s/S, t/T)."
+ },
+ "output": {
+ "title": "Lệnh",
+ "copy": "Sao chép",
+ "copied": "Đã sao chép"
+ },
+ "explain": {
+ "line": "{cls} có thể {perms}.",
+ "none": "{cls} không có quyền nào.",
+ "setuid": "Setuid: tệp chạy với đặc quyền của chủ sở hữu.",
+ "setgid": "Setgid: tệp chạy với đặc quyền của nhóm (các tệp mới trong thư mục kế thừa nhóm của nó).",
+ "sticky": "Sticky: chỉ chủ sở hữu tệp mới có thể xóa hoặc đổi tên tệp trong thư mục này."
+ },
+ "presets": {
+ "title": "Cài đặt sẵn phổ biến",
+ "octal": "Bát phân",
+ "permissions": "Quyền",
+ "description": "Công dụng điển hình",
+ "p644": "Tệp thông thường: chủ sở hữu ghi, mọi người đọc.",
+ "p755": "Tệp thực thi và thư mục: mọi người có thể đọc và truy cập.",
+ "p600": "Tệp riêng tư: chỉ chủ sở hữu đọc và ghi.",
+ "p700": "Tệp thực thi và thư mục riêng tư: chỉ chủ sở hữu.",
+ "p664": "Tệp cho phép nhóm ghi.",
+ "p775": "Thư mục và tệp thực thi cho phép nhóm ghi.",
+ "p400": "Chỉ đọc cho chủ sở hữu (ví dụ khóa riêng SSH)."
+ }
+ },
+ "CodeScreenshot": {
+ "title": "Ảnh chụp mã",
+ "subtitle": "Biến mã thành hình ảnh đẹp, dễ chia sẻ",
+ "languageLabel": "Ngôn ngữ",
+ "autoDetect": "Tự động nhận diện",
+ "backgroundLabel": "Nền",
+ "paddingLabel": "Khoảng đệm",
+ "filenameLabel": "Tên tệp",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "Chấm cửa sổ",
+ "themeLabel": "Chủ đề cửa sổ",
+ "themeDark": "Tối",
+ "themeLight": "Sáng",
+ "scaleLabel": "Tỷ lệ xuất",
+ "copyImage": "Sao chép hình ảnh",
+ "copied": "Đã sao chép",
+ "exportPng": "Xuất PNG",
+ "inputLabel": "Mã",
+ "detected": "Đã nhận diện: {language}",
+ "clear": "Xóa",
+ "placeholder": "Dán mã của bạn vào đây…",
+ "previewLabel": "Xem trước",
+ "emptyPreview": "Bản xem trước mã của bạn sẽ hiển thị ở đây",
+ "exportFailed": "Không xuất được hình ảnh",
+ "copyUnsupported": "Trình duyệt này không hỗ trợ sao chép hình ảnh",
+ "copyFailed": "Không sao chép được hình ảnh",
+ "backgrounds": {
+ "ocean": "Đại dương",
+ "sunset": "Hoàng hôn",
+ "candy": "Kẹo ngọt",
+ "forest": "Rừng cây",
+ "midnight": "Nửa đêm",
+ "gold": "Vàng kim",
+ "aurora": "Cực quang",
+ "slate": "Xám đá",
+ "transparent": "Trong suốt"
+ }
+ },
+ "CssGenerators": {
+ "title": "Trình tạo CSS",
+ "subtitle": "Box-shadow, border-radius và clamp() linh hoạt — sao chép CSS.",
+ "preview": "Xem trước",
+ "output": "CSS",
+ "copy": "Sao chép",
+ "copied": "Đã sao chép",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "clamp() linh hoạt"
+ },
+ "boxShadow": {
+ "presets": "Cài đặt sẵn",
+ "layer": "Lớp",
+ "addLayer": "Thêm lớp",
+ "removeLayer": "Xóa lớp",
+ "x": "X",
+ "y": "Y",
+ "blur": "Độ mờ",
+ "spread": "Độ lan",
+ "color": "Màu",
+ "alpha": "Độ trong",
+ "inset": "Đổ bóng trong",
+ "presetNames": {
+ "subtleCard": "Thẻ nhẹ nhàng",
+ "elevated": "Nổi lên",
+ "hardOffset": "Lệch cứng",
+ "neonGlow": "Ánh neon",
+ "inner": "Bóng trong",
+ "layeredSmooth": "Nhiều lớp mượt"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "Chế độ",
+ "modes": {
+ "simple": "Theo từng góc",
+ "blob": "Khối hữu cơ"
+ },
+ "unit": "Đơn vị",
+ "linkCorners": "Liên kết các góc",
+ "randomize": "Ngẫu nhiên",
+ "horizontal": "Bán kính ngang",
+ "vertical": "Bán kính dọc",
+ "corners": {
+ "tl": "Trên trái",
+ "tr": "Trên phải",
+ "br": "Dưới phải",
+ "bl": "Dưới trái"
+ }
+ },
+ "clamp": {
+ "minFont": "Cỡ chữ tối thiểu (px)",
+ "maxFont": "Cỡ chữ tối đa (px)",
+ "minViewport": "Viewport tối thiểu (px)",
+ "maxViewport": "Viewport tối đa (px)",
+ "rootFont": "Cỡ chữ gốc (px)",
+ "viewportWidth": "Viewport mô phỏng",
+ "computedSize": "Kích thước tính toán",
+ "slope": "Độ dốc",
+ "intercept": "Giao điểm",
+ "previewText": "The quick brown fox",
+ "invalid": "Nhập các giới hạn viewport hợp lệ và khác nhau."
+ }
+ },
+ "ExifViewer": {
+ "title": "Trình xem & xóa EXIF",
+ "subtitle": "Kiểm tra metadata của ảnh và xóa chúng ngay trong trình duyệt",
+ "dropTitle": "Thả một ảnh vào đây",
+ "dropHint": "hoặc nhấn để duyệt — ảnh của bạn không bao giờ rời khỏi thiết bị",
+ "browse": "Chọn ảnh",
+ "formats": "JPEG, PNG, WebP, HEIC, TIFF",
+ "parsing": "Đang đọc metadata…",
+ "parseFailed": "Không đọc được ảnh này",
+ "clear": "Xóa ảnh",
+ "gpsWarning": "Chứa vị trí GPS",
+ "openMap": "Xem trên OpenStreetMap",
+ "noMetadata": "Không tìm thấy metadata nhúng nào trong ảnh này.",
+ "facts": {
+ "name": "Tên",
+ "type": "Loại",
+ "dimensions": "Kích thước",
+ "size": "Dung lượng"
+ },
+ "groups": {
+ "camera": "Máy ảnh",
+ "exposure": "Phơi sáng",
+ "timestamps": "Dấu thời gian",
+ "gps": "Vị trí GPS",
+ "software": "Phần mềm & khác"
+ },
+ "gpsCard": {
+ "latitude": "Vĩ độ",
+ "longitude": "Kinh độ",
+ "altitude": "Độ cao"
+ },
+ "raw": {
+ "title": "Toàn bộ metadata",
+ "copyJson": "Sao chép JSON",
+ "copied": "Đã sao chép"
+ },
+ "remove": {
+ "title": "Xóa metadata",
+ "jpegDescription": "Xóa EXIF, XMP, IPTC và chú thích không mất dữ liệu mà không nén lại ảnh.",
+ "reencodeDescription": "Mã hóa lại ảnh để loại bỏ metadata nhúng. Thao tác này nén lại các điểm ảnh.",
+ "keepIcc": "Giữ hồ sơ màu (ICC)",
+ "stripButton": "Xóa metadata",
+ "reencodeButton": "Mã hóa lại & xóa",
+ "unsupported": "Không hỗ trợ xóa metadata cho loại tệp này.",
+ "failed": "Không xóa được metadata",
+ "before": "Trước",
+ "after": "Sau",
+ "download": "Tải ảnh đã làm sạch"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Trình tạo Favicon",
+ "subtitle": "Tạo favicon và biểu tượng ứng dụng từ một ảnh",
+ "dropTitle": "Thả một ảnh vào đây",
+ "dropHint": "PNG, JPEG hoặc SVG — nhấn để duyệt",
+ "browse": "Chọn ảnh",
+ "clear": "Xóa ảnh",
+ "backgroundLabel": "Nền",
+ "transparent": "Trong suốt",
+ "solidColor": "Màu đặc",
+ "cornerLabel": "Hình dạng góc",
+ "cornerNone": "Vuông",
+ "cornerRounded": "Bo tròn",
+ "cornerCircle": "Hình tròn",
+ "maskPaddingLabel": "Khoảng đệm maskable",
+ "maskable": "maskable",
+ "previewLabel": "Xem trước",
+ "copy": "Sao chép",
+ "headSnippet": "Đoạn mã HTML head",
+ "downloadIco": "Tải favicon.ico",
+ "downloadZip": "Tải tất cả (.zip)"
+ },
+ "JsonDiff": {
+ "title": "So sánh JSON",
+ "subtitle": "So sánh hai tài liệu JSON theo ngữ nghĩa và cấu trúc — bỏ qua thứ tự khóa và định dạng",
+ "options": {
+ "ignoreArrayOrder": "Bỏ qua thứ tự mảng"
+ },
+ "loadBothSamples": "Tải ví dụ",
+ "sample": "Ví dụ",
+ "format": "Định dạng",
+ "clear": "Xóa",
+ "formatError": "Không thể định dạng: đầu vào không phải JSON hợp lệ",
+ "placeholder": "Dán hoặc nhập JSON vào đây…",
+ "panels": {
+ "left": "Bản gốc (trái)",
+ "right": "Bản sửa (phải)",
+ "results": "Khác biệt"
+ },
+ "summary": {
+ "added": "{count} đã thêm",
+ "removed": "{count} đã xóa",
+ "changed": "{count} đã thay đổi"
+ },
+ "types": {
+ "added": "Đã thêm",
+ "removed": "Đã xóa",
+ "changed": "Đã thay đổi"
+ },
+ "copyChanges": "Sao chép dưới dạng JSON",
+ "copied": "Đã sao chép",
+ "empty": "Dán JSON vào cả hai ô để xem khác biệt của chúng.",
+ "identical": "Hai tài liệu giống nhau về mặt ngữ nghĩa."
+ },
+ "JsonpathPlayground": {
+ "title": "Sân chơi JSONPath",
+ "subtitle": "Truy vấn các tài liệu JSON bằng JSONPath hoặc JMESPath và xem kết quả trực tiếp",
+ "engineLabel": "Bộ máy",
+ "queryLabel": "Truy vấn",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "Bảng tra cứu nhanh",
+ "cheats": {
+ "jpAllTitles": "Tất cả tựa sách",
+ "jpAllAuthors": "Mọi tác giả, ở bất kỳ đâu trong tài liệu",
+ "jpFirstBook": "Cuốn sách đầu tiên",
+ "jpLastBook": "Cuốn sách cuối cùng",
+ "jpSlice": "Hai cuốn đầu (slice)",
+ "jpFilterPrice": "Sách rẻ hơn 40",
+ "jpFilterInStock": "Tựa của các cuốn sách còn hàng",
+ "jpRecursive": "Tất cả giá trị, theo đệ quy",
+ "jmAllTitles": "Tất cả tựa sách",
+ "jmFirstAuthor": "Tác giả của cuốn sách đầu tiên",
+ "jmLastBook": "Cuốn sách cuối cùng",
+ "jmSlice": "Hai cuốn đầu (slice)",
+ "jmFilterPrice": "Tựa của các cuốn sách rẻ hơn 40",
+ "jmCount": "Đếm số sách còn hàng",
+ "jmProjection": "Định hình lại mỗi cuốn sách thành đối tượng mới",
+ "jmSort": "Tựa sách sắp xếp theo giá"
+ },
+ "panels": {
+ "input": "Đầu vào JSON",
+ "results": "Kết quả"
+ },
+ "sample": "Ví dụ",
+ "format": "Định dạng",
+ "clear": "Xóa",
+ "formatError": "Không thể định dạng: đầu vào không phải JSON hợp lệ",
+ "placeholder": "Dán hoặc nhập JSON vào đây…",
+ "outputPlaceholder": "Các kết quả khớp sẽ hiển thị ở đây…",
+ "matchCount": "{count, plural, one {# kết quả khớp} other {# kết quả khớp}}",
+ "copy": "Sao chép",
+ "copied": "Đã sao chép"
+ },
+ "KeycodeInspector": {
+ "title": "Trình kiểm tra mã phím",
+ "subtitle": "Nhấn phím bất kỳ để kiểm tra sự kiện bàn phím JavaScript của nó",
+ "capture": {
+ "prompt": "Nhấn phím bất kỳ…",
+ "ready": "Đang lắng nghe — nhấn phím bất kỳ trên bàn phím",
+ "clickToFocus": "Nhấn vào đây để lấy tiêu điểm, rồi nhấn phím bất kỳ"
+ },
+ "hero": {
+ "spaceKey": "(Dấu cách)",
+ "keyLabel": "event.key",
+ "repeat": "lặp lại"
+ },
+ "copyJson": "Sao chép JSON",
+ "copiedJson": "Đã sao chép JSON sự kiện vào bộ nhớ tạm",
+ "deprecated": "không dùng nữa",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "Chuẩn",
+ "left": "Trái",
+ "right": "Phải",
+ "numpad": "Bàn phím số",
+ "unknown": "Không rõ"
+ },
+ "history": {
+ "title": "Lịch sử",
+ "clear": "Xóa lịch sử",
+ "empty": "Các lần nhấn phím sẽ hiển thị ở đây."
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Trình tạo bảng Markdown",
+ "subtitle": "Chỉnh sửa lưới, dán từ bảng tính và sao chép bảng Markdown hoặc HTML được căn chỉnh đẹp mắt",
+ "addRow": "Thêm hàng",
+ "addColumn": "Thêm cột",
+ "importLabel": "Nhập",
+ "importPlaceholder": "Dán TSV (từ Excel / Google Sheets) hoặc CSV vào đây…",
+ "importButton": "Nhập vào lưới",
+ "pasteClipboard": "Dán từ bộ nhớ tạm",
+ "importEmpty": "Không có gì để nhập — hãy dán một số hàng trước",
+ "clipboardError": "Không thể đọc từ bộ nhớ tạm",
+ "clear": "Xóa",
+ "panels": {
+ "editor": "Trình chỉnh sửa bảng",
+ "output": "Kết quả"
+ },
+ "align": {
+ "left": "Căn trái",
+ "center": "Căn giữa",
+ "right": "Căn phải"
+ },
+ "headerPlaceholder": "Tiêu đề",
+ "cellPlaceholder": "Ô",
+ "removeRow": "Xóa hàng",
+ "removeColumn": "Xóa cột",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "Sao chép",
+ "copied": "Đã sao chép"
+ },
+ "TimezoneConverter": {
+ "title": "Chuyển đổi múi giờ",
+ "subtitle": "Chuyển đổi một thời điểm trên khắp thế giới để lên kế hoạch họp.",
+ "dateTimeLabel": "Ngày & giờ",
+ "now": "Bây giờ",
+ "sourceZoneLabel": "Múi giờ nguồn",
+ "selectZone": "Chọn múi giờ",
+ "searchZone": "Tìm múi giờ...",
+ "noZone": "Không tìm thấy múi giờ.",
+ "scrubLabel": "Dịch chuyển thời gian",
+ "reset": "Đặt lại",
+ "targetsLabel": "Múi giờ đích",
+ "addZone": "Thêm múi giờ",
+ "remove": "Xóa",
+ "localBadge": "Địa phương",
+ "invalidDate": "Nhập ngày và giờ hợp lệ.",
+ "bands": {
+ "work": "Giờ làm việc (09:00–18:00)",
+ "edge": "Giờ giáp ranh (sáng sớm / tối)",
+ "night": "Ban đêm"
+ }
+ },
+ "TokenCounter": {
+ "title": "Bộ đếm token LLM",
+ "subtitle": "Đếm token, trực quan hóa các khối và ước tính chi phí API.",
+ "inputLabel": "Văn bản",
+ "sample": "Ví dụ",
+ "clear": "Xóa",
+ "placeholder": "Dán văn bản, mã hoặc JSON để đếm token...",
+ "encoding": "Mã hóa",
+ "loading": "Đang tải bộ tách token...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / dòng o)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Token",
+ "characters": "Ký tự",
+ "words": "Từ",
+ "ratio": "Token/từ",
+ "ratioSub": "xấp xỉ"
+ },
+ "claudeRow": "Claude (xấp xỉ)",
+ "estimate": "ước tính",
+ "viz": {
+ "title": "Trực quan hóa token",
+ "toggle": "Hiển thị",
+ "hint": "Bật để tô màu mỗi token thành một khối.",
+ "empty": "Nhập văn bản để trực quan hóa token.",
+ "truncated": "Đang hiển thị {limit} token đầu tiên trong tổng số {total}."
+ },
+ "cost": {
+ "title": "Máy tính chi phí",
+ "disclaimer": "giá trị mặc định có thể sửa — hãy kiểm tra giá hiện tại",
+ "model": "Mô hình",
+ "expectedOutput": "Số token đầu ra dự kiến",
+ "customInput": "Đầu vào $/MTok",
+ "customOutput": "Đầu ra $/MTok",
+ "inputCost": "Chi phí đầu vào",
+ "outputCost": "Chi phí đầu ra",
+ "total": "Tổng",
+ "per1000": "Mỗi 1000 lần gọi"
+ }
+ },
+ "WebhookTester": {
+ "title": "Trình kiểm tra Webhook",
+ "subtitle": "Tạo một URL tạm thời và kiểm tra các yêu cầu webhook đến trực tiếp.",
+ "intro": {
+ "title": "Tạo một URL webhook để bắt đầu ghi lại các yêu cầu",
+ "hint": "Bạn sẽ nhận được một URL không thể đoán, ghi lại mọi yêu cầu đến. Nó hết hạn sau 24 giờ."
+ },
+ "createBtn": "Tạo URL webhook",
+ "creating": "Đang tạo…",
+ "urlLabel": "URL webhook của bạn",
+ "expiresIn": "Hết hạn sau {countdown}",
+ "copyUrlBtn": "Sao chép URL",
+ "copied": "Đã sao chép",
+ "clearBtn": "Xóa",
+ "newBinBtn": "URL mới",
+ "requestsTitle": "Yêu cầu",
+ "selectPrompt": "Chọn một yêu cầu để xem chi tiết.",
+ "expiredNotice": "URL webhook này đã hết hạn. Tạo một URL mới để tiếp tục kiểm tra.",
+ "base64Badge": "base64",
+ "truncatedBadge": "đã cắt bớt",
+ "empty": {
+ "title": "Đang chờ yêu cầu…",
+ "hint": "Gửi một yêu cầu đến URL của bạn và nó sẽ hiển thị ngay tại đây. Hãy thử:"
+ },
+ "detail": {
+ "receivedAt": "Nhận lúc",
+ "source": "IP nguồn",
+ "contentType": "Content-Type",
+ "query": "Tham số truy vấn",
+ "headers": "Header",
+ "body": "Nội dung",
+ "copyBody": "Sao chép nội dung",
+ "emptyBody": "Không có nội dung"
+ },
+ "errors": {
+ "createFailed": "Không tạo được URL webhook. Vui lòng thử lại.",
+ "clearFailed": "Không xóa được các yêu cầu. Vui lòng thử lại."
+ }
+ },
+ "WebsocketTester": {
+ "title": "Trình kiểm tra WebSocket",
+ "subtitle": "Kết nối tới một điểm cuối WebSocket và gửi, nhận, kiểm tra tin nhắn trực tiếp",
+ "urlLabel": "URL điểm cuối",
+ "protocolsLabel": "Giao thức phụ",
+ "protocolsPlaceholder": "tùy chọn, phân tách bằng dấu phẩy",
+ "connect": "Kết nối",
+ "disconnect": "Ngắt kết nối",
+ "status": {
+ "disconnected": "Đã ngắt kết nối",
+ "connecting": "Đang kết nối",
+ "connected": "Đã kết nối",
+ "error": "Lỗi"
+ },
+ "messages": {
+ "connecting": "Đang kết nối tới {url}…",
+ "connected": "Đã thiết lập kết nối",
+ "connectedWithProtocol": "Đã thiết lập kết nối (giao thức: {protocol})",
+ "invalidUrl": "URL không hợp lệ — phải bắt đầu bằng ws:// hoặc wss://",
+ "connectFailed": "Không mở được kết nối",
+ "socketError": "Đã xảy ra lỗi kết nối",
+ "notConnected": "Chưa kết nối — hãy kết nối trước",
+ "binaryMessage": "Đã nhận một tin nhắn nhị phân"
+ },
+ "log": {
+ "title": "Nhật ký tin nhắn",
+ "empty": "Chưa có tin nhắn. Hãy kết nối và bắt đầu gửi.",
+ "copyAll": "Sao chép tất cả",
+ "download": "Tải xuống dạng .txt",
+ "clear": "Xóa nhật ký",
+ "prettyPrint": "Định dạng đẹp JSON",
+ "showRaw": "Hiện văn bản thô"
+ },
+ "composer": {
+ "placeholder": "Nhập tin nhắn để gửi…",
+ "send": "Gửi",
+ "formatJson": "Định dạng thành JSON",
+ "invalidJson": "Tin nhắn không phải JSON hợp lệ",
+ "hint": "Cmd/Ctrl+Enter để gửi"
+ },
+ "ping": {
+ "label": "Tự động ping",
+ "seconds": "giây",
+ "intervalTitle": "Khoảng ping tính bằng giây",
+ "payloadPlaceholder": "payload ping"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Tra cứu Whois",
+ "subtitle": "Tra cứu thông tin đăng ký cho bất kỳ tên miền hoặc địa chỉ IP nào qua RDAP",
+ "placeholder": "example.com hoặc 8.8.8.8",
+ "lookupBtn": "Tra cứu",
+ "looking": "Đang tra cứu…",
+ "refreshBtn": "Làm mới",
+ "tryLabel": "Thử:",
+ "copy": "Sao chép",
+ "copied": "Đã sao chép",
+ "notAvailable": "Không có sẵn",
+ "summary": {
+ "registrar": "Nhà đăng ký",
+ "network": "Mạng"
+ },
+ "dates": {
+ "created": "Ngày tạo",
+ "updated": "Ngày cập nhật",
+ "expires": "Ngày hết hạn"
+ },
+ "sections": {
+ "status": "Trạng thái",
+ "nameservers": "Máy chủ tên",
+ "contacts": "Liên hệ",
+ "rawRdap": "RDAP thô"
+ },
+ "contacts": {
+ "roles": "Vai trò",
+ "name": "Tên",
+ "organization": "Tổ chức"
+ },
+ "errors": {
+ "invalidTarget": "Nhập một tên miền hoặc địa chỉ IP hợp lệ",
+ "lookupFailed": "Tra cứu thất bại (trạng thái {status})"
+ }
}
}
diff --git a/apps/web/messages/zh.json b/apps/web/messages/zh.json
index 1d18872c..db0036ec 100644
--- a/apps/web/messages/zh.json
+++ b/apps/web/messages/zh.json
@@ -61,7 +61,22 @@
"stringCaseConverter": "String Case Converter",
"lineSortDedupe": "Line Sort & Dedupe",
"stringInspector": "String Inspector",
- "escapeEncode": "Escape / Encode"
+ "escapeEncode": "Escape / Encode",
+ "bcryptGenerator": "Bcrypt",
+ "chmodCalculator": "Chmod 计算器",
+ "codeScreenshot": "代码截图",
+ "cssGenerators": "CSS 生成器",
+ "exifViewer": "EXIF 查看器",
+ "faviconGenerator": "Favicon 生成器",
+ "jsonDiff": "JSON 对比",
+ "jsonpathPlayground": "JSONPath 沙盒",
+ "keycodeInspector": "键码检查器",
+ "markdownTableGenerator": "Markdown 表格",
+ "timezoneConverter": "时区转换器",
+ "tokenCounter": "Token 计数器",
+ "webhookTester": "Webhook 测试器",
+ "websocketTester": "WebSocket 测试器",
+ "whoisLookup": "Whois 查询"
},
"Help": {
"title": "帮助与文档",
@@ -526,6 +541,66 @@
"escapeEncode": {
"title": "Escape / Encode",
"description": "Escape and unescape HTML entities, backslash strings, hex, and Unicode."
+ },
+ "bcryptGenerator": {
+ "title": "Bcrypt 生成与校验",
+ "description": "使用 bcrypt 哈希密码并校验现有哈希值。"
+ },
+ "chmodCalculator": {
+ "title": "Chmod 计算器",
+ "description": "在八进制、符号表示和复选框之间转换 Unix 权限。"
+ },
+ "codeScreenshot": {
+ "title": "代码截图",
+ "description": "将源代码转换为精美、可分享的图片。"
+ },
+ "cssGenerators": {
+ "title": "CSS 生成器",
+ "description": "Box-shadow、border-radius 和流式 clamp() 生成器。"
+ },
+ "exifViewer": {
+ "title": "EXIF 查看与移除",
+ "description": "在本地检查并清除照片的 EXIF 元数据。"
+ },
+ "faviconGenerator": {
+ "title": "Favicon 生成器",
+ "description": "生成 favicon、应用图标和清单代码片段。"
+ },
+ "jsonDiff": {
+ "title": "JSON 对比",
+ "description": "对两个 JSON 文档进行语义化、结构感知的对比。"
+ },
+ "jsonpathPlayground": {
+ "title": "JSONPath 沙盒",
+ "description": "使用 JSONPath 或 JMESPath 查询 JSON 并实时查看结果。"
+ },
+ "keycodeInspector": {
+ "title": "键码检查器",
+ "description": "按下按键即可查看其 JavaScript 事件码和修饰键。"
+ },
+ "markdownTableGenerator": {
+ "title": "Markdown 表格生成器",
+ "description": "在类似电子表格的网格编辑器中构建 Markdown 和 HTML 表格。"
+ },
+ "timezoneConverter": {
+ "title": "时区转换器",
+ "description": "跨多个时区转换时间,并提示日期偏移和工作时间。"
+ },
+ "tokenCounter": {
+ "title": "LLM Token 计数器",
+ "description": "统计 LLM token 数量、可视化分块并估算 API 成本。"
+ },
+ "webhookTester": {
+ "title": "Webhook 测试器",
+ "description": "实时捕获并检查传入的 webhook 请求。"
+ },
+ "websocketTester": {
+ "title": "WebSocket 测试器",
+ "description": "连接 WebSocket 端点并实时收发消息。"
+ },
+ "whoisLookup": {
+ "title": "Whois 查询",
+ "description": "通过 RDAP 查询域名和 IP 的注册信息。"
}
},
"tabs": {
@@ -3877,5 +3952,602 @@
"copy": "Copy",
"copied": "Copied",
"outputPlaceholder": "Result will appear here…"
+ },
+ "BcryptGenerator": {
+ "title": "Bcrypt 生成与校验",
+ "subtitle": "使用 bcrypt 哈希密码并校验哈希 — 一切都在你的浏览器中完成",
+ "tabs": {
+ "hash": "哈希",
+ "verify": "校验"
+ },
+ "showPassword": "显示密码",
+ "hidePassword": "隐藏密码",
+ "copy": "复制",
+ "copied": "已复制",
+ "hash": {
+ "passwordLabel": "密码",
+ "passwordPlaceholder": "输入要哈希的密码…",
+ "costLabel": "成本因子",
+ "costHint": "更高的成本因子会使每一步的计算量翻倍。生产环境常用 10-12。",
+ "slowWarning": "成本因子达到 13 及以上可能需要数秒才能计算完成。",
+ "generate": "生成哈希",
+ "generating": "正在哈希…",
+ "resultLabel": "Bcrypt 哈希",
+ "timeTaken": "{ms} 毫秒",
+ "anatomy": {
+ "title": "哈希结构",
+ "algorithm": "算法",
+ "cost": "成本",
+ "salt": "盐值(22 个字符)",
+ "hash": "哈希(31 个字符)"
+ }
+ },
+ "verify": {
+ "passwordLabel": "密码",
+ "passwordPlaceholder": "输入要校验的密码…",
+ "hashLabel": "Bcrypt 哈希",
+ "hashPlaceholder": "$2b$10$…",
+ "idle": "输入密码和 bcrypt 哈希以进行比对。",
+ "invalidHash": "这看起来不是有效的 bcrypt 哈希。",
+ "checking": "正在比对…",
+ "match": "匹配 — 该密码可生成此哈希。",
+ "noMatch": "不匹配 — 该密码无法生成此哈希。"
+ }
+ },
+ "ChmodCalculator": {
+ "title": "Chmod 计算器",
+ "subtitle": "在复选框、八进制和符号表示之间转换 Unix 文件权限",
+ "grid": {
+ "title": "权限",
+ "read": "读取",
+ "write": "写入",
+ "execute": "执行",
+ "owner": "所有者",
+ "group": "组",
+ "others": "其他用户"
+ },
+ "special": {
+ "title": "特殊位",
+ "setuid": "Setuid",
+ "setgid": "Setgid",
+ "sticky": "Sticky"
+ },
+ "octal": {
+ "label": "八进制",
+ "placeholder": "755 或 4755",
+ "invalid": "请输入 3 或 4 位八进制数字(0-7)。"
+ },
+ "symbolic": {
+ "label": "符号表示",
+ "placeholder": "rwxr-xr-x",
+ "invalid": "请输入 9 个字符,如 rwxr-xr-x(允许 s/S、t/T)。"
+ },
+ "output": {
+ "title": "命令",
+ "copy": "复制",
+ "copied": "已复制"
+ },
+ "explain": {
+ "line": "{cls}可以{perms}。",
+ "none": "{cls}没有任何权限。",
+ "setuid": "Setuid:文件以其所有者的权限运行。",
+ "setgid": "Setgid:文件以其所属组的权限运行(目录中的新文件会继承该组)。",
+ "sticky": "Sticky:只有文件的所有者才能在此目录中删除或重命名它。"
+ },
+ "presets": {
+ "title": "常用预设",
+ "octal": "八进制",
+ "permissions": "权限",
+ "description": "典型用途",
+ "p644": "普通文件:所有者可写,所有人可读。",
+ "p755": "可执行文件和目录:所有人可读取和进入。",
+ "p600": "私有文件:只有所有者可读写。",
+ "p700": "私有可执行文件和目录:仅限所有者。",
+ "p664": "组可写文件。",
+ "p775": "组可写目录和可执行文件。",
+ "p400": "所有者只读(例如 SSH 私钥)。"
+ }
+ },
+ "CodeScreenshot": {
+ "title": "代码截图",
+ "subtitle": "将代码转换为精美、可分享的图片",
+ "languageLabel": "语言",
+ "autoDetect": "自动检测",
+ "backgroundLabel": "背景",
+ "paddingLabel": "内边距",
+ "filenameLabel": "文件名",
+ "filenamePlaceholder": "example.ts",
+ "windowLabel": "窗口圆点",
+ "themeLabel": "窗口主题",
+ "themeDark": "深色",
+ "themeLight": "浅色",
+ "scaleLabel": "导出倍率",
+ "copyImage": "复制图片",
+ "copied": "已复制",
+ "exportPng": "导出 PNG",
+ "inputLabel": "代码",
+ "detected": "已检测:{language}",
+ "clear": "清空",
+ "placeholder": "在此粘贴你的代码…",
+ "previewLabel": "预览",
+ "emptyPreview": "你的代码预览将显示在这里",
+ "exportFailed": "导出图片失败",
+ "copyUnsupported": "此浏览器不支持复制图片",
+ "copyFailed": "复制图片失败",
+ "backgrounds": {
+ "ocean": "海洋",
+ "sunset": "日落",
+ "candy": "糖果",
+ "forest": "森林",
+ "midnight": "午夜",
+ "gold": "金色",
+ "aurora": "极光",
+ "slate": "石板灰",
+ "transparent": "透明"
+ }
+ },
+ "CssGenerators": {
+ "title": "CSS 生成器",
+ "subtitle": "Box-shadow、border-radius 和流式 clamp() — 复制 CSS。",
+ "preview": "预览",
+ "output": "CSS",
+ "copy": "复制",
+ "copied": "已复制",
+ "tabs": {
+ "boxShadow": "Box Shadow",
+ "borderRadius": "Border Radius",
+ "clamp": "流式 clamp()"
+ },
+ "boxShadow": {
+ "presets": "预设",
+ "layer": "图层",
+ "addLayer": "添加图层",
+ "removeLayer": "移除图层",
+ "x": "X",
+ "y": "Y",
+ "blur": "模糊",
+ "spread": "扩展",
+ "color": "颜色",
+ "alpha": "透明度",
+ "inset": "内阴影",
+ "presetNames": {
+ "subtleCard": "轻微卡片",
+ "elevated": "悬浮",
+ "hardOffset": "硬偏移",
+ "neonGlow": "霓虹光晕",
+ "inner": "内阴影",
+ "layeredSmooth": "多层柔和"
+ }
+ },
+ "borderRadius": {
+ "modeLabel": "模式",
+ "modes": {
+ "simple": "逐角",
+ "blob": "有机形状"
+ },
+ "unit": "单位",
+ "linkCorners": "关联各角",
+ "randomize": "随机",
+ "horizontal": "水平半径",
+ "vertical": "垂直半径",
+ "corners": {
+ "tl": "左上",
+ "tr": "右上",
+ "br": "右下",
+ "bl": "左下"
+ }
+ },
+ "clamp": {
+ "minFont": "最小字号 (px)",
+ "maxFont": "最大字号 (px)",
+ "minViewport": "最小视口 (px)",
+ "maxViewport": "最大视口 (px)",
+ "rootFont": "根字号 (px)",
+ "viewportWidth": "模拟视口",
+ "computedSize": "计算尺寸",
+ "slope": "斜率",
+ "intercept": "截距",
+ "previewText": "The quick brown fox",
+ "invalid": "请输入有效且不同的视口范围。"
+ }
+ },
+ "ExifViewer": {
+ "title": "EXIF 查看与移除",
+ "subtitle": "在浏览器中检查照片元数据并将其清除",
+ "dropTitle": "将图片拖放到此处",
+ "dropHint": "或点击浏览 — 你的图片绝不会离开你的设备",
+ "browse": "选择图片",
+ "formats": "JPEG、PNG、WebP、HEIC、TIFF",
+ "parsing": "正在读取元数据…",
+ "parseFailed": "读取此图片失败",
+ "clear": "移除图片",
+ "gpsWarning": "包含 GPS 位置信息",
+ "openMap": "在 OpenStreetMap 上查看",
+ "noMetadata": "此图片中未找到嵌入的元数据。",
+ "facts": {
+ "name": "名称",
+ "type": "类型",
+ "dimensions": "尺寸",
+ "size": "大小"
+ },
+ "groups": {
+ "camera": "相机",
+ "exposure": "曝光",
+ "timestamps": "时间戳",
+ "gps": "GPS 位置",
+ "software": "软件与其他"
+ },
+ "gpsCard": {
+ "latitude": "纬度",
+ "longitude": "经度",
+ "altitude": "海拔"
+ },
+ "raw": {
+ "title": "全部元数据",
+ "copyJson": "复制 JSON",
+ "copied": "已复制"
+ },
+ "remove": {
+ "title": "移除元数据",
+ "jpegDescription": "无损移除 EXIF、XMP、IPTC 和注释,且不重新压缩图片。",
+ "reencodeDescription": "重新编码图片以清除嵌入的元数据。这会重新压缩像素。",
+ "keepIcc": "保留颜色配置文件 (ICC)",
+ "stripButton": "清除元数据",
+ "reencodeButton": "重新编码并清除",
+ "unsupported": "此文件类型不支持移除元数据。",
+ "failed": "移除元数据失败",
+ "before": "之前",
+ "after": "之后",
+ "download": "下载已清理的图片"
+ }
+ },
+ "FaviconGenerator": {
+ "title": "Favicon 生成器",
+ "subtitle": "从一张图片创建 favicon 和应用图标",
+ "dropTitle": "将图片拖放到此处",
+ "dropHint": "PNG、JPEG 或 SVG — 点击浏览",
+ "browse": "选择图片",
+ "clear": "移除图片",
+ "backgroundLabel": "背景",
+ "transparent": "透明",
+ "solidColor": "纯色",
+ "cornerLabel": "圆角形状",
+ "cornerNone": "方形",
+ "cornerRounded": "圆角",
+ "cornerCircle": "圆形",
+ "maskPaddingLabel": "可遮罩内边距",
+ "maskable": "maskable",
+ "previewLabel": "预览",
+ "copy": "复制",
+ "headSnippet": "HTML head 代码片段",
+ "downloadIco": "下载 favicon.ico",
+ "downloadZip": "下载全部 (.zip)"
+ },
+ "JsonDiff": {
+ "title": "JSON 对比",
+ "subtitle": "对两个 JSON 文档进行语义化、结构感知的比较 — 忽略键顺序和格式",
+ "options": {
+ "ignoreArrayOrder": "忽略数组顺序"
+ },
+ "loadBothSamples": "加载示例",
+ "sample": "示例",
+ "format": "格式化",
+ "clear": "清空",
+ "formatError": "无法格式化:输入不是有效的 JSON",
+ "placeholder": "在此粘贴或输入 JSON…",
+ "panels": {
+ "left": "原始(左)",
+ "right": "修改后(右)",
+ "results": "差异"
+ },
+ "summary": {
+ "added": "{count} 项新增",
+ "removed": "{count} 项移除",
+ "changed": "{count} 项变更"
+ },
+ "types": {
+ "added": "新增",
+ "removed": "移除",
+ "changed": "变更"
+ },
+ "copyChanges": "复制为 JSON",
+ "copied": "已复制",
+ "empty": "在两个面板中粘贴 JSON 以查看它们的差异。",
+ "identical": "这两个文档在语义上完全相同。"
+ },
+ "JsonpathPlayground": {
+ "title": "JSONPath 沙盒",
+ "subtitle": "使用 JSONPath 或 JMESPath 查询 JSON 文档并实时查看结果",
+ "engineLabel": "引擎",
+ "queryLabel": "查询",
+ "engines": {
+ "jsonpath": "JSONPath",
+ "jmespath": "JMESPath"
+ },
+ "cheatsheet": "速查表",
+ "cheats": {
+ "jpAllTitles": "所有书名",
+ "jpAllAuthors": "文档中任意位置的所有作者",
+ "jpFirstBook": "第一本书",
+ "jpLastBook": "最后一本书",
+ "jpSlice": "前两本书(切片)",
+ "jpFilterPrice": "价格低于 40 的书",
+ "jpFilterInStock": "有库存书籍的书名",
+ "jpRecursive": "递归获取所有值",
+ "jmAllTitles": "所有书名",
+ "jmFirstAuthor": "第一本书的作者",
+ "jmLastBook": "最后一本书",
+ "jmSlice": "前两本书(切片)",
+ "jmFilterPrice": "价格低于 40 的书名",
+ "jmCount": "统计有库存的书籍",
+ "jmProjection": "将每本书重塑为新对象",
+ "jmSort": "按价格排序的书名"
+ },
+ "panels": {
+ "input": "JSON 输入",
+ "results": "结果"
+ },
+ "sample": "示例",
+ "format": "格式化",
+ "clear": "清空",
+ "formatError": "无法格式化:输入不是有效的 JSON",
+ "placeholder": "在此粘贴或输入 JSON…",
+ "outputPlaceholder": "匹配结果将显示在这里…",
+ "matchCount": "{count, plural, one {# 个匹配} other {# 个匹配}}",
+ "copy": "复制",
+ "copied": "已复制"
+ },
+ "KeycodeInspector": {
+ "title": "键码检查器",
+ "subtitle": "按下任意键以检查其 JavaScript 键盘事件",
+ "capture": {
+ "prompt": "按下任意键…",
+ "ready": "正在监听 — 请按键盘上的任意键",
+ "clickToFocus": "点击此处以聚焦,然后按任意键"
+ },
+ "hero": {
+ "spaceKey": "(空格)",
+ "keyLabel": "event.key",
+ "repeat": "重复"
+ },
+ "copyJson": "复制 JSON",
+ "copiedJson": "事件 JSON 已复制到剪贴板",
+ "deprecated": "已弃用",
+ "fields": {
+ "code": "event.code",
+ "keyCode": "event.keyCode",
+ "which": "event.which",
+ "location": "event.location"
+ },
+ "location": {
+ "standard": "标准",
+ "left": "左",
+ "right": "右",
+ "numpad": "小键盘",
+ "unknown": "未知"
+ },
+ "history": {
+ "title": "历史记录",
+ "clear": "清空历史记录",
+ "empty": "按键记录将显示在这里。"
+ }
+ },
+ "MarkdownTableGenerator": {
+ "title": "Markdown 表格生成器",
+ "subtitle": "编辑网格、从电子表格粘贴,并复制对齐美观的 Markdown 或 HTML 表格",
+ "addRow": "添加行",
+ "addColumn": "添加列",
+ "importLabel": "导入",
+ "importPlaceholder": "在此粘贴 TSV(来自 Excel / Google Sheets)或 CSV…",
+ "importButton": "导入到网格",
+ "pasteClipboard": "从剪贴板粘贴",
+ "importEmpty": "无内容可导入 — 请先粘贴一些行",
+ "clipboardError": "无法从剪贴板读取",
+ "clear": "清空",
+ "panels": {
+ "editor": "表格编辑器",
+ "output": "输出"
+ },
+ "align": {
+ "left": "左对齐",
+ "center": "居中对齐",
+ "right": "右对齐"
+ },
+ "headerPlaceholder": "表头",
+ "cellPlaceholder": "单元格",
+ "removeRow": "移除行",
+ "removeColumn": "移除列",
+ "tabs": {
+ "markdown": "Markdown",
+ "html": "HTML"
+ },
+ "copy": "复制",
+ "copied": "已复制"
+ },
+ "TimezoneConverter": {
+ "title": "时区转换器",
+ "subtitle": "在全球范围内转换某一时刻,方便安排会议。",
+ "dateTimeLabel": "日期和时间",
+ "now": "现在",
+ "sourceZoneLabel": "源时区",
+ "selectZone": "选择时区",
+ "searchZone": "搜索时区...",
+ "noZone": "未找到时区。",
+ "scrubLabel": "调整时间",
+ "reset": "重置",
+ "targetsLabel": "目标时区",
+ "addZone": "添加时区",
+ "remove": "移除",
+ "localBadge": "本地",
+ "invalidDate": "请输入有效的日期和时间。",
+ "bands": {
+ "work": "工作时间(09:00–18:00)",
+ "edge": "边缘时间(清晨 / 傍晚)",
+ "night": "夜间"
+ }
+ },
+ "TokenCounter": {
+ "title": "LLM Token 计数器",
+ "subtitle": "统计 token、可视化分块并估算 API 成本。",
+ "inputLabel": "文本",
+ "sample": "示例",
+ "clear": "清空",
+ "placeholder": "粘贴文本、代码或 JSON 以统计 token...",
+ "encoding": "编码",
+ "loading": "正在加载分词器...",
+ "encodings": {
+ "o200k": "o200k_base (GPT-4o / o 系列)",
+ "cl100k": "cl100k_base (GPT-4 / 3.5)"
+ },
+ "stats": {
+ "tokens": "Token 数",
+ "characters": "字符数",
+ "words": "单词数",
+ "ratio": "Token/单词",
+ "ratioSub": "约"
+ },
+ "claudeRow": "Claude(近似)",
+ "estimate": "估算",
+ "viz": {
+ "title": "Token 可视化",
+ "toggle": "显示",
+ "hint": "切换以将每个 token 着色为一个分块。",
+ "empty": "输入文本以可视化 token。",
+ "truncated": "显示 {total} 个 token 中的前 {limit} 个。"
+ },
+ "cost": {
+ "title": "成本计算器",
+ "disclaimer": "可编辑的默认值 — 请核对当前定价",
+ "model": "模型",
+ "expectedOutput": "预期输出 token 数",
+ "customInput": "输入 $/MTok",
+ "customOutput": "输出 $/MTok",
+ "inputCost": "输入成本",
+ "outputCost": "输出成本",
+ "total": "总计",
+ "per1000": "每 1000 次调用"
+ }
+ },
+ "WebhookTester": {
+ "title": "Webhook 测试器",
+ "subtitle": "创建临时 URL 并实时检查传入的 webhook 请求。",
+ "intro": {
+ "title": "创建 webhook URL 以开始捕获请求",
+ "hint": "你将获得一个无法猜测的 URL,它会记录每个传入的请求。24 小时后过期。"
+ },
+ "createBtn": "创建 webhook URL",
+ "creating": "正在创建…",
+ "urlLabel": "你的 webhook URL",
+ "expiresIn": "{countdown} 后过期",
+ "copyUrlBtn": "复制 URL",
+ "copied": "已复制",
+ "clearBtn": "清空",
+ "newBinBtn": "新建 URL",
+ "requestsTitle": "请求",
+ "selectPrompt": "选择一个请求以查看其详细信息。",
+ "expiredNotice": "此 webhook URL 已过期。创建一个新的以继续测试。",
+ "base64Badge": "base64",
+ "truncatedBadge": "已截断",
+ "empty": {
+ "title": "正在等待请求…",
+ "hint": "向你的 URL 发送请求,它会立即显示在这里。试试:"
+ },
+ "detail": {
+ "receivedAt": "接收时间",
+ "source": "来源 IP",
+ "contentType": "Content-Type",
+ "query": "查询参数",
+ "headers": "请求头",
+ "body": "正文",
+ "copyBody": "复制正文",
+ "emptyBody": "无正文"
+ },
+ "errors": {
+ "createFailed": "创建 webhook URL 失败。请重试。",
+ "clearFailed": "清空请求失败。请重试。"
+ }
+ },
+ "WebsocketTester": {
+ "title": "WebSocket 测试器",
+ "subtitle": "连接 WebSocket 端点并实时发送、接收和检查消息",
+ "urlLabel": "端点 URL",
+ "protocolsLabel": "子协议",
+ "protocolsPlaceholder": "可选,以逗号分隔",
+ "connect": "连接",
+ "disconnect": "断开连接",
+ "status": {
+ "disconnected": "已断开",
+ "connecting": "正在连接",
+ "connected": "已连接",
+ "error": "错误"
+ },
+ "messages": {
+ "connecting": "正在连接到 {url}…",
+ "connected": "连接已建立",
+ "connectedWithProtocol": "连接已建立(协议:{protocol})",
+ "invalidUrl": "无效的 URL — 必须以 ws:// 或 wss:// 开头",
+ "connectFailed": "打开连接失败",
+ "socketError": "发生连接错误",
+ "notConnected": "未连接 — 请先连接",
+ "binaryMessage": "收到二进制消息"
+ },
+ "log": {
+ "title": "消息日志",
+ "empty": "暂无消息。连接后开始发送。",
+ "copyAll": "全部复制",
+ "download": "下载为 .txt",
+ "clear": "清空日志",
+ "prettyPrint": "美化 JSON",
+ "showRaw": "显示原始文本"
+ },
+ "composer": {
+ "placeholder": "输入要发送的消息…",
+ "send": "发送",
+ "formatJson": "格式化为 JSON",
+ "invalidJson": "消息不是有效的 JSON",
+ "hint": "Cmd/Ctrl+Enter 发送"
+ },
+ "ping": {
+ "label": "自动 ping",
+ "seconds": "秒",
+ "intervalTitle": "Ping 间隔(秒)",
+ "payloadPlaceholder": "ping 负载"
+ }
+ },
+ "WhoisLookup": {
+ "title": "Whois 查询",
+ "subtitle": "通过 RDAP 查询任意域名或 IP 地址的注册信息",
+ "placeholder": "example.com 或 8.8.8.8",
+ "lookupBtn": "查询",
+ "looking": "正在查询…",
+ "refreshBtn": "刷新",
+ "tryLabel": "试试:",
+ "copy": "复制",
+ "copied": "已复制",
+ "notAvailable": "不可用",
+ "summary": {
+ "registrar": "注册商",
+ "network": "网络"
+ },
+ "dates": {
+ "created": "创建时间",
+ "updated": "更新时间",
+ "expires": "过期时间"
+ },
+ "sections": {
+ "status": "状态",
+ "nameservers": "名称服务器",
+ "contacts": "联系人",
+ "rawRdap": "原始 RDAP"
+ },
+ "contacts": {
+ "roles": "角色",
+ "name": "名称",
+ "organization": "组织"
+ },
+ "errors": {
+ "invalidTarget": "请输入有效的域名或 IP 地址",
+ "lookupFailed": "查询失败(状态 {status})"
+ }
}
}
diff --git a/apps/web/src/app/app/bcrypt-generator/layout.tsx b/apps/web/src/app/app/bcrypt-generator/layout.tsx
new file mode 100644
index 00000000..13739967
--- /dev/null
+++ b/apps/web/src/app/app/bcrypt-generator/layout.tsx
@@ -0,0 +1,7 @@
+import { generateToolMetadata } from '@/lib/metadata'
+
+export const metadata = generateToolMetadata('bcrypt-generator')
+
+export default function BcryptGeneratorLayout({ children }: { children: React.ReactNode }) {
+ return <>{children}>
+}
diff --git a/apps/web/src/app/app/bcrypt-generator/page.tsx b/apps/web/src/app/app/bcrypt-generator/page.tsx
new file mode 100644
index 00000000..92bcc676
--- /dev/null
+++ b/apps/web/src/app/app/bcrypt-generator/page.tsx
@@ -0,0 +1,8 @@
+import { BcryptGeneratorLayout } from '@/components/bcrypt-generator/bcrypt-generator-layout'
+export default function BcryptGeneratorPage() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/web/src/app/app/chmod-calculator/layout.tsx b/apps/web/src/app/app/chmod-calculator/layout.tsx
new file mode 100644
index 00000000..a1f79691
--- /dev/null
+++ b/apps/web/src/app/app/chmod-calculator/layout.tsx
@@ -0,0 +1,7 @@
+import { generateToolMetadata } from '@/lib/metadata'
+
+export const metadata = generateToolMetadata('chmod-calculator')
+
+export default function ChmodCalculatorPageLayout({ children }: { children: React.ReactNode }) {
+ return <>{children}>
+}
diff --git a/apps/web/src/app/app/chmod-calculator/page.tsx b/apps/web/src/app/app/chmod-calculator/page.tsx
new file mode 100644
index 00000000..5da12b2b
--- /dev/null
+++ b/apps/web/src/app/app/chmod-calculator/page.tsx
@@ -0,0 +1,8 @@
+import { ChmodCalculatorLayout } from '@/components/chmod-calculator/chmod-calculator-layout'
+export default function ChmodCalculatorPage() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/web/src/app/app/code-screenshot/layout.tsx b/apps/web/src/app/app/code-screenshot/layout.tsx
new file mode 100644
index 00000000..e5ea6c16
--- /dev/null
+++ b/apps/web/src/app/app/code-screenshot/layout.tsx
@@ -0,0 +1,7 @@
+import { generateToolMetadata } from '@/lib/metadata'
+
+export const metadata = generateToolMetadata('code-screenshot')
+
+export default function CodeScreenshotLayout({ children }: { children: React.ReactNode }) {
+ return <>{children}>
+}
diff --git a/apps/web/src/app/app/code-screenshot/page.tsx b/apps/web/src/app/app/code-screenshot/page.tsx
new file mode 100644
index 00000000..31f536f4
--- /dev/null
+++ b/apps/web/src/app/app/code-screenshot/page.tsx
@@ -0,0 +1,8 @@
+import { CodeScreenshotLayout } from '@/components/code-screenshot/code-screenshot-layout'
+export default function CodeScreenshotPage() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/web/src/app/app/css-generators/layout.tsx b/apps/web/src/app/app/css-generators/layout.tsx
new file mode 100644
index 00000000..3e525519
--- /dev/null
+++ b/apps/web/src/app/app/css-generators/layout.tsx
@@ -0,0 +1,7 @@
+import { generateToolMetadata } from '@/lib/metadata'
+
+export const metadata = generateToolMetadata('css-generators')
+
+export default function CssGeneratorsLayout({ children }: { children: React.ReactNode }) {
+ return <>{children}>
+}
diff --git a/apps/web/src/app/app/css-generators/page.tsx b/apps/web/src/app/app/css-generators/page.tsx
new file mode 100644
index 00000000..3299ab0a
--- /dev/null
+++ b/apps/web/src/app/app/css-generators/page.tsx
@@ -0,0 +1,8 @@
+import { CssGeneratorsLayout } from '@/components/css-generators/css-generators-layout'
+export default function CssGeneratorsPage() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/web/src/app/app/exif-viewer/layout.tsx b/apps/web/src/app/app/exif-viewer/layout.tsx
new file mode 100644
index 00000000..24e2caa0
--- /dev/null
+++ b/apps/web/src/app/app/exif-viewer/layout.tsx
@@ -0,0 +1,7 @@
+import { generateToolMetadata } from '@/lib/metadata'
+
+export const metadata = generateToolMetadata('exif-viewer')
+
+export default function ExifViewerLayout({ children }: { children: React.ReactNode }) {
+ return <>{children}>
+}
diff --git a/apps/web/src/app/app/exif-viewer/page.tsx b/apps/web/src/app/app/exif-viewer/page.tsx
new file mode 100644
index 00000000..397dc391
--- /dev/null
+++ b/apps/web/src/app/app/exif-viewer/page.tsx
@@ -0,0 +1,8 @@
+import { ExifViewerLayout } from '@/components/exif-viewer/exif-viewer-layout'
+export default function ExifViewerPage() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/web/src/app/app/favicon-generator/layout.tsx b/apps/web/src/app/app/favicon-generator/layout.tsx
new file mode 100644
index 00000000..0ac209dd
--- /dev/null
+++ b/apps/web/src/app/app/favicon-generator/layout.tsx
@@ -0,0 +1,7 @@
+import { generateToolMetadata } from '@/lib/metadata'
+
+export const metadata = generateToolMetadata('favicon-generator')
+
+export default function FaviconGeneratorLayout({ children }: { children: React.ReactNode }) {
+ return <>{children}>
+}
diff --git a/apps/web/src/app/app/favicon-generator/page.tsx b/apps/web/src/app/app/favicon-generator/page.tsx
new file mode 100644
index 00000000..25375774
--- /dev/null
+++ b/apps/web/src/app/app/favicon-generator/page.tsx
@@ -0,0 +1,8 @@
+import { FaviconGeneratorLayout } from '@/components/favicon-generator/favicon-generator-layout'
+export default function FaviconGeneratorPage() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/web/src/app/app/json-diff/layout.tsx b/apps/web/src/app/app/json-diff/layout.tsx
new file mode 100644
index 00000000..f2fa2040
--- /dev/null
+++ b/apps/web/src/app/app/json-diff/layout.tsx
@@ -0,0 +1,7 @@
+import { generateToolMetadata } from '@/lib/metadata'
+
+export const metadata = generateToolMetadata('json-diff')
+
+export default function JsonDiffLayout({ children }: { children: React.ReactNode }) {
+ return <>{children}>
+}
diff --git a/apps/web/src/app/app/json-diff/page.tsx b/apps/web/src/app/app/json-diff/page.tsx
new file mode 100644
index 00000000..cd6f243b
--- /dev/null
+++ b/apps/web/src/app/app/json-diff/page.tsx
@@ -0,0 +1,8 @@
+import { JsonDiffLayout } from '@/components/json-diff/json-diff-layout'
+export default function JsonDiffPage() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/web/src/app/app/jsonpath-playground/layout.tsx b/apps/web/src/app/app/jsonpath-playground/layout.tsx
new file mode 100644
index 00000000..1233d37f
--- /dev/null
+++ b/apps/web/src/app/app/jsonpath-playground/layout.tsx
@@ -0,0 +1,7 @@
+import { generateToolMetadata } from '@/lib/metadata'
+
+export const metadata = generateToolMetadata('jsonpath-playground')
+
+export default function JsonpathPlaygroundLayout({ children }: { children: React.ReactNode }) {
+ return <>{children}>
+}
diff --git a/apps/web/src/app/app/jsonpath-playground/page.tsx b/apps/web/src/app/app/jsonpath-playground/page.tsx
new file mode 100644
index 00000000..f4379786
--- /dev/null
+++ b/apps/web/src/app/app/jsonpath-playground/page.tsx
@@ -0,0 +1,8 @@
+import { JsonpathPlaygroundLayout } from '@/components/jsonpath-playground/jsonpath-playground-layout'
+export default function JsonpathPlaygroundPage() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/web/src/app/app/keycode-inspector/layout.tsx b/apps/web/src/app/app/keycode-inspector/layout.tsx
new file mode 100644
index 00000000..13436707
--- /dev/null
+++ b/apps/web/src/app/app/keycode-inspector/layout.tsx
@@ -0,0 +1,7 @@
+import { generateToolMetadata } from '@/lib/metadata'
+
+export const metadata = generateToolMetadata('keycode-inspector')
+
+export default function KeycodeInspectorPageLayout({ children }: { children: React.ReactNode }) {
+ return <>{children}>
+}
diff --git a/apps/web/src/app/app/keycode-inspector/page.tsx b/apps/web/src/app/app/keycode-inspector/page.tsx
new file mode 100644
index 00000000..f6413354
--- /dev/null
+++ b/apps/web/src/app/app/keycode-inspector/page.tsx
@@ -0,0 +1,8 @@
+import { KeycodeInspectorLayout } from '@/components/keycode-inspector/keycode-inspector-layout'
+export default function KeycodeInspectorPage() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/web/src/app/app/markdown-table-generator/layout.tsx b/apps/web/src/app/app/markdown-table-generator/layout.tsx
new file mode 100644
index 00000000..6bda40c2
--- /dev/null
+++ b/apps/web/src/app/app/markdown-table-generator/layout.tsx
@@ -0,0 +1,7 @@
+import { generateToolMetadata } from '@/lib/metadata'
+
+export const metadata = generateToolMetadata('markdown-table-generator')
+
+export default function MarkdownTableGeneratorLayout({ children }: { children: React.ReactNode }) {
+ return <>{children}>
+}
diff --git a/apps/web/src/app/app/markdown-table-generator/page.tsx b/apps/web/src/app/app/markdown-table-generator/page.tsx
new file mode 100644
index 00000000..531b5b6f
--- /dev/null
+++ b/apps/web/src/app/app/markdown-table-generator/page.tsx
@@ -0,0 +1,8 @@
+import { MarkdownTableGeneratorLayout } from '@/components/markdown-table-generator/markdown-table-generator-layout'
+export default function MarkdownTableGeneratorPage() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/web/src/app/app/timezone-converter/layout.tsx b/apps/web/src/app/app/timezone-converter/layout.tsx
new file mode 100644
index 00000000..0c0778c7
--- /dev/null
+++ b/apps/web/src/app/app/timezone-converter/layout.tsx
@@ -0,0 +1,7 @@
+import { generateToolMetadata } from '@/lib/metadata'
+
+export const metadata = generateToolMetadata('timezone-converter')
+
+export default function TimezoneConverterLayout({ children }: { children: React.ReactNode }) {
+ return <>{children}>
+}
diff --git a/apps/web/src/app/app/timezone-converter/page.tsx b/apps/web/src/app/app/timezone-converter/page.tsx
new file mode 100644
index 00000000..57e3a08e
--- /dev/null
+++ b/apps/web/src/app/app/timezone-converter/page.tsx
@@ -0,0 +1,8 @@
+import { TimezoneConverterLayout } from '@/components/timezone-converter/timezone-converter-layout'
+export default function TimezoneConverterPage() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/web/src/app/app/to-do/components/AssigneePicker.tsx b/apps/web/src/app/app/to-do/components/AssigneePicker.tsx
index 74f26ce9..80e4b467 100644
--- a/apps/web/src/app/app/to-do/components/AssigneePicker.tsx
+++ b/apps/web/src/app/app/to-do/components/AssigneePicker.tsx
@@ -29,7 +29,7 @@ import {
memberLabel,
memberInitials,
} from "@/app/app/to-do/hooks/useWorkspaceMembers";
-import type { Member } from "@/lib/members-api";
+import type { Member } from "@/app/app/to-do/hooks/useWorkspaceMembers";
const SIZES = {
sm: { avatar: "h-6 w-6", text: "text-[10px]", icon: "h-4 w-4" },
diff --git a/apps/web/src/app/app/to-do/hooks/useWorkspaceMembers.ts b/apps/web/src/app/app/to-do/hooks/useWorkspaceMembers.ts
index 75552112..b7754ae4 100644
--- a/apps/web/src/app/app/to-do/hooks/useWorkspaceMembers.ts
+++ b/apps/web/src/app/app/to-do/hooks/useWorkspaceMembers.ts
@@ -1,29 +1,39 @@
"use client";
-import { useQuery } from "@tanstack/react-query";
-import { listWorkspaceMembers, type Member } from "@/lib/members-api";
-import { useActiveWorkspace } from "@/store/workspace-store";
+import useAuth from "@/utils/useAuth";
+
+export type Member = {
+ uid: string;
+ email: string | null;
+ display_name: string | null;
+ role: string;
+ since: number;
+};
/**
- * Members of the active workspace, for assignee pickers. Reuses the existing
- * workspace members API — no new backend endpoint. In a personal workspace the
- * list is just the owner; assignment shines in shared workspaces.
+ * Members available for assignee pickers. Workspaces are single-user (personal)
+ * now that shared/org collaboration has been removed, so the only assignable
+ * member is the current user.
*/
export function useWorkspaceMembers() {
- const ws = useActiveWorkspace();
- const wsId = ws?.id ?? null;
+ const { user } = useAuth();
- const query = useQuery({
- queryKey: ["workspaceMembers", wsId],
- queryFn: () => listWorkspaceMembers(wsId as string),
- enabled: !!wsId,
- staleTime: 5 * 60 * 1000, // members change rarely
- });
+ const members: Member[] = user
+ ? [
+ {
+ uid: user.uid,
+ email: user.email ?? null,
+ display_name: user.displayName ?? null,
+ role: "admin",
+ since: 0,
+ },
+ ]
+ : [];
return {
- members: query.data ?? [],
- isLoading: query.isPending && !!wsId,
- isShared: !!ws && !ws.is_personal,
+ members,
+ isLoading: false,
+ isShared: false,
};
}
diff --git a/apps/web/src/app/app/token-counter/layout.tsx b/apps/web/src/app/app/token-counter/layout.tsx
new file mode 100644
index 00000000..3b96fd99
--- /dev/null
+++ b/apps/web/src/app/app/token-counter/layout.tsx
@@ -0,0 +1,7 @@
+import { generateToolMetadata } from '@/lib/metadata'
+
+export const metadata = generateToolMetadata('token-counter')
+
+export default function TokenCounterLayout({ children }: { children: React.ReactNode }) {
+ return <>{children}>
+}
diff --git a/apps/web/src/app/app/token-counter/page.tsx b/apps/web/src/app/app/token-counter/page.tsx
new file mode 100644
index 00000000..eca3de9d
--- /dev/null
+++ b/apps/web/src/app/app/token-counter/page.tsx
@@ -0,0 +1,8 @@
+import { TokenCounterLayout } from '@/components/token-counter/token-counter-layout'
+export default function TokenCounterPage() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/web/src/app/app/webhook-tester/layout.tsx b/apps/web/src/app/app/webhook-tester/layout.tsx
new file mode 100644
index 00000000..e8a126ea
--- /dev/null
+++ b/apps/web/src/app/app/webhook-tester/layout.tsx
@@ -0,0 +1,7 @@
+import { generateToolMetadata } from '@/lib/metadata'
+
+export const metadata = generateToolMetadata('webhook-tester')
+
+export default function WebhookTesterLayout({ children }: { children: React.ReactNode }) {
+ return <>{children}>
+}
diff --git a/apps/web/src/app/app/webhook-tester/page.tsx b/apps/web/src/app/app/webhook-tester/page.tsx
new file mode 100644
index 00000000..deae037a
--- /dev/null
+++ b/apps/web/src/app/app/webhook-tester/page.tsx
@@ -0,0 +1,8 @@
+import { WebhookTesterLayout } from '@/components/webhook-tester/webhook-tester-layout'
+export default function WebhookTesterPage() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/web/src/app/app/websocket-tester/layout.tsx b/apps/web/src/app/app/websocket-tester/layout.tsx
new file mode 100644
index 00000000..369573d1
--- /dev/null
+++ b/apps/web/src/app/app/websocket-tester/layout.tsx
@@ -0,0 +1,7 @@
+import { generateToolMetadata } from '@/lib/metadata'
+
+export const metadata = generateToolMetadata('websocket-tester')
+
+export default function WebsocketTesterLayout({ children }: { children: React.ReactNode }) {
+ return <>{children}>
+}
diff --git a/apps/web/src/app/app/websocket-tester/page.tsx b/apps/web/src/app/app/websocket-tester/page.tsx
new file mode 100644
index 00000000..a3cefcac
--- /dev/null
+++ b/apps/web/src/app/app/websocket-tester/page.tsx
@@ -0,0 +1,8 @@
+import { WebsocketTesterLayout } from '@/components/websocket-tester/websocket-tester-layout'
+export default function WebsocketTesterPage() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/web/src/app/app/whois-lookup/layout.tsx b/apps/web/src/app/app/whois-lookup/layout.tsx
new file mode 100644
index 00000000..33190431
--- /dev/null
+++ b/apps/web/src/app/app/whois-lookup/layout.tsx
@@ -0,0 +1,7 @@
+import { generateToolMetadata } from '@/lib/metadata'
+
+export const metadata = generateToolMetadata('whois-lookup')
+
+export default function WhoisLookupLayout({ children }: { children: React.ReactNode }) {
+ return <>{children}>
+}
diff --git a/apps/web/src/app/app/whois-lookup/page.tsx b/apps/web/src/app/app/whois-lookup/page.tsx
new file mode 100644
index 00000000..4e0060e7
--- /dev/null
+++ b/apps/web/src/app/app/whois-lookup/page.tsx
@@ -0,0 +1,8 @@
+import { WhoisLookupLayout } from '@/components/whois-lookup/whois-lookup-layout'
+export default function WhoisLookupPage() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/web/src/app/dashboard/page.tsx b/apps/web/src/app/dashboard/page.tsx
index 56ec2053..6df8dc43 100644
--- a/apps/web/src/app/dashboard/page.tsx
+++ b/apps/web/src/app/dashboard/page.tsx
@@ -2,7 +2,6 @@
import React, { useState, useEffect, useMemo } from 'react'
import dynamic from 'next/dynamic'
-import Link from 'next/link'
import { ArrowLeft, ArrowRight, BarChart3, LayoutGrid } from 'lucide-react'
import { useTranslations } from 'next-intl'
import { sidebarData } from '@/components/sidebar/data/sidebar-data'
@@ -10,7 +9,6 @@ import { usePinnedToolsStore, usePinnedToolsForActiveWorkspace } from '@/store/p
import { useWorkspaceStore } from '@/store/workspace-store'
import { useToolUsage } from '@/hooks/use-tool-usage'
import useAuth from '@/utils/useAuth'
-import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { TooltipProvider } from '@/components/ui/tooltip'
@@ -29,7 +27,6 @@ import { DashboardPinnedSection } from '@/components/dashboard/dashboard-pinned-
import { DashboardWhatsNew } from '@/components/dashboard/dashboard-whats-new'
import { DashboardRecentTools } from '@/components/dashboard/dashboard-recent-tools'
import { ActivityLogDrawer } from '@/components/dashboard/activity/activity-log-drawer'
-import { DashboardLoginCta } from '@/components/dashboard/dashboard-login-cta'
import { DashboardToolGrid } from '@/components/dashboard/dashboard-tool-grid'
import { DashboardFeatured } from '@/components/dashboard/dashboard-featured'
import { RevealItem } from '@/components/dashboard/dashboard-reveal'
@@ -59,7 +56,6 @@ const DashboardAnalyticsPanel = dynamic(
// ─── Dashboard Page ─────────────────────────────────────────────────────────
const DashboardPage: React.FC = () => {
- const t = useTranslations('Dashboard')
const tTabs = useTranslations('Dashboard.tabs')
const { user, loading } = useAuth(false)
const pinnedToolUrls = usePinnedToolsForActiveWorkspace()
@@ -360,11 +356,6 @@ const DashboardPage: React.FC = () => {
/>
- {/* Login CTA (non-auth) */}
-
-
-
-
{/* Browse-all entry — the door to the full catalog */}