initialization
This commit is contained in:
commit
9ff09693ed
92 files changed
+46209
No files matched your search
@@ -0,0 +1,18 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.github
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
logs/
|
||||||
|
vendor/
|
||||||
|
node_modules/
|
||||||
|
coverage/
|
||||||
|
.DS_Store
|
||||||
|
**/*.log
|
||||||
|
**/*.tmp
|
||||||
|
**/*.bak
|
||||||
|
**/~*
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Local development defaults
|
||||||
|
APP_ENV=development
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_NAME="Password Generator API"
|
||||||
|
APP_VERSION="2.0.0"
|
||||||
|
|
||||||
|
# Security
|
||||||
|
API_KEY_ENABLED=false
|
||||||
|
API_KEY=""
|
||||||
|
|
||||||
|
# CORS (keep local development origin explicit instead of wildcard)
|
||||||
|
CORS_ORIGIN="http://localhost:8080"
|
||||||
|
CORS_METHODS="GET,POST,OPTIONS"
|
||||||
|
CORS_HEADERS="Content-Type,Authorization"
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
LOG_PATH=logs/app.log
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
RATE_LIMIT=60
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
cache/
|
||||||
|
vendor/
|
||||||
|
config/config.local.php
|
||||||
|
logs/app.log
|
||||||
|
agent-prompts
|
||||||
|
.env
|
||||||
|
~*
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Agent Instructions: Password Generator API & Web App
|
||||||
|
|
||||||
|
This project is a stateless PHP 8.4 portal for issuing device setup payloads and provisioning configuration for authenticated users via Authentik OIDC.
|
||||||
|
|
||||||
|
This project is about creating a modern, secure password generator with REST API and interactive multilingual web frontend. It must be built with a Slim PHP Framework, featuring multiple password generation algorithms, comprehensive password manager integrations, and full internationalization support.
|
||||||
|
|
||||||
|
## Core Workflows & Responsibilities
|
||||||
|
|
||||||
|
The project has two main features
|
||||||
|
|
||||||
|
1. A multilingual web UI for the user to interactively create passwords using different algorithms. It must be intuitively understood and make it very easy for the user to select and copy a generated password of choice.
|
||||||
|
2. REST API to make the different password generation algorithms available via a well documented API. The API must be documents with a Swagger OpenAPI 3 specifications.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
Some algorithms rely on the shell commands xkcdpass and pwgen so these preferable should be available although this functionality is represented in PHP as a fallback as well.
|
||||||
|
|
||||||
|
## Deployments
|
||||||
|
|
||||||
|
Deployments is preferably via a docker image but can be done manually on bare metal as well. Both approaches must be possible and well documented.
|
||||||
|
|
||||||
|
The docker image must contain the necessary shell commands.
|
||||||
+303
@@ -0,0 +1,303 @@
|
|||||||
|
# Deployment Guide
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Installation
|
||||||
|
```bash
|
||||||
|
# Make installation script executable
|
||||||
|
chmod +x install.sh
|
||||||
|
|
||||||
|
# Run installation
|
||||||
|
./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Development Server
|
||||||
|
```bash
|
||||||
|
# Start with Composer
|
||||||
|
composer start
|
||||||
|
|
||||||
|
# Or manually
|
||||||
|
php -S localhost:8080 -t public
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Access Application
|
||||||
|
- **Web Interface**: http://localhost:8080
|
||||||
|
- **API Documentation**: http://localhost:8080/docs
|
||||||
|
- **Health Check**: http://localhost:8080/api/v1/health
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
### Apache Configuration
|
||||||
|
```apache
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName passwords.yourdomain.com
|
||||||
|
DocumentRoot /var/www/password-generator/public
|
||||||
|
|
||||||
|
<Directory /var/www/password-generator/public>
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
Header always set X-Content-Type-Options nosniff
|
||||||
|
Header always set X-Frame-Options DENY
|
||||||
|
Header always set X-XSS-Protection "1; mode=block"
|
||||||
|
</VirtualHost>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nginx Configuration
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name passwords.yourdomain.com;
|
||||||
|
root /var/www/password-generator/public;
|
||||||
|
index index.php;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
add_header X-Frame-Options DENY;
|
||||||
|
add_header X-XSS-Protection "1; mode=block";
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg)$ {
|
||||||
|
expires 1M;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Deployment
|
||||||
|
```dockerfile
|
||||||
|
FROM php:8.4-cli
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
ENV APP_ENV=production \
|
||||||
|
APP_DEBUG=false \
|
||||||
|
PORT=8080
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
bash \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
unzip \
|
||||||
|
pwgen \
|
||||||
|
xkcdpass \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
COPY . /var/www/html/
|
||||||
|
|
||||||
|
RUN composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist \
|
||||||
|
&& mkdir -p logs \
|
||||||
|
&& chmod 755 logs \
|
||||||
|
&& cp -n .env.example .env
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||||
|
CMD curl -fsS http://127.0.0.1:8080/ >/dev/null || exit 1
|
||||||
|
|
||||||
|
CMD ["php", "-S", "0.0.0.0:8080", "-t", "public"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
password-generator:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
APP_ENV: production
|
||||||
|
APP_DEBUG: "false"
|
||||||
|
LOG_LEVEL: warning
|
||||||
|
RATE_LIMIT: "120"
|
||||||
|
CORS_ORIGIN: "https://yourdomain.com"
|
||||||
|
CORS_METHODS: "GET,POST,OPTIONS"
|
||||||
|
CORS_HEADERS: "Content-Type,Authorization"
|
||||||
|
volumes:
|
||||||
|
- ./logs:/var/www/html/logs
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Configuration
|
||||||
|
|
||||||
|
### Production Environment Variables
|
||||||
|
```bash
|
||||||
|
# Application
|
||||||
|
APP_ENV=production
|
||||||
|
APP_DEBUG=false
|
||||||
|
APP_NAME="Password Generator API"
|
||||||
|
APP_VERSION="2.0.0"
|
||||||
|
APP_PUBLIC_BASE_URL="https://passwords.example.com"
|
||||||
|
|
||||||
|
# Security
|
||||||
|
API_KEY_ENABLED=false
|
||||||
|
RATE_LIMIT=120
|
||||||
|
|
||||||
|
# CORS (adjust for your domain)
|
||||||
|
CORS_ORIGIN="https://yourdomain.com"
|
||||||
|
CORS_METHODS="GET,POST,OPTIONS"
|
||||||
|
CORS_HEADERS="Content-Type,Authorization"
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL=warning
|
||||||
|
LOG_PATH=logs/app.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Secure Production Baseline
|
||||||
|
Use local development values from `.env.example` and set deployment values explicitly in the production environment. The application does not assume a public-safe default; it expects a secure deployment configuration to be provided at runtime.
|
||||||
|
|
||||||
|
Production checklist:
|
||||||
|
- [ ] `APP_ENV=production`
|
||||||
|
- [ ] `APP_DEBUG=false`
|
||||||
|
- [ ] `LOG_LEVEL=warning` or `error`
|
||||||
|
- [ ] `CORS_ORIGIN` restricted to trusted frontend domains only
|
||||||
|
- [ ] `RATE_LIMIT` tuned for the production deployment and not left at dev defaults
|
||||||
|
- [ ] `require_https=true` enforced at the reverse proxy or load balancer
|
||||||
|
- [ ] TLS termination with HSTS and strict security headers in front of the PHP app
|
||||||
|
- [ ] Secrets and runtime hostnames stored outside the repository
|
||||||
|
|
||||||
|
Remaining production hardening risks:
|
||||||
|
- In-memory rate limiting is still single-instance only; multi-node deployments should use shared storage.
|
||||||
|
- CORS must remain an explicit allow-list and should not be widened to wildcard origins.
|
||||||
|
- Log aggregation and retention should be added for production operations.
|
||||||
|
- Browser-facing security headers should be applied at the edge layer whenever the app is public.
|
||||||
|
|
||||||
|
### Regenerate the OpenAPI File
|
||||||
|
The public OpenAPI document is generated from the live route registry and the configured public base URL. Regenerate it after route changes or deployment configuration changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer generate:openapi
|
||||||
|
# or
|
||||||
|
php utils/generate-openapi.php
|
||||||
|
```
|
||||||
|
|
||||||
|
This updates `public/pwgenerator-swagger-spec.json` with the current public API contract.
|
||||||
|
|
||||||
|
### SSL/TLS Configuration
|
||||||
|
For production, always use HTTPS:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name passwords.yourdomain.com;
|
||||||
|
|
||||||
|
ssl_certificate /path/to/certificate.crt;
|
||||||
|
ssl_certificate_key /path/to/private.key;
|
||||||
|
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
|
||||||
|
# ... rest of configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
# Redirect HTTP to HTTPS
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name passwords.yourdomain.com;
|
||||||
|
return 301 https://$server_name$request_uri;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring and Maintenance
|
||||||
|
|
||||||
|
### Health Checks
|
||||||
|
```bash
|
||||||
|
# API health check
|
||||||
|
curl -f http://localhost:8080/api/v1/health || exit 1
|
||||||
|
|
||||||
|
# Test password generation
|
||||||
|
curl -X POST http://localhost:8080/api/v1/passwords/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"type":"high-entropy","count":1}' \
|
||||||
|
| jq '.success'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Monitoring
|
||||||
|
```bash
|
||||||
|
# Monitor application logs
|
||||||
|
tail -f logs/app.log
|
||||||
|
|
||||||
|
# Check for errors
|
||||||
|
grep -i error logs/app.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Optimization
|
||||||
|
```bash
|
||||||
|
# Enable OPcache for PHP
|
||||||
|
echo "opcache.enable=1" >> /etc/php/8.1/apache2/conf.d/99-opcache.ini
|
||||||
|
echo "opcache.memory_consumption=128" >> /etc/php/8.1/apache2/conf.d/99-opcache.ini
|
||||||
|
|
||||||
|
# Restart Apache
|
||||||
|
systemctl restart apache2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Rate Limiting**: Adjust `RATE_LIMIT` based on your needs
|
||||||
|
2. **CORS**: Configure `CORS_ORIGIN` for your domain only
|
||||||
|
3. **HTTPS**: Always use SSL/TLS in production
|
||||||
|
4. **Firewall**: Restrict access to necessary ports only
|
||||||
|
5. **Updates**: Keep PHP and dependencies updated
|
||||||
|
6. **Logs**: Monitor logs for suspicious activity
|
||||||
|
|
||||||
|
## Backup Strategy
|
||||||
|
|
||||||
|
### Files to Backup
|
||||||
|
- Configuration files (`.env`)
|
||||||
|
- Custom wordlists (if added)
|
||||||
|
- Application logs
|
||||||
|
- Web server configuration
|
||||||
|
|
||||||
|
### Database
|
||||||
|
This application is stateless and doesn't require database backups.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Error: "Class not found"**
|
||||||
|
```bash
|
||||||
|
composer dump-autoload
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error: "Permission denied on logs"**
|
||||||
|
```bash
|
||||||
|
chmod 755 logs/
|
||||||
|
chown www-data:www-data logs/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error: "CORS blocked"**
|
||||||
|
- Check `CORS_ORIGIN` in `.env`
|
||||||
|
- Ensure proper headers in web server config
|
||||||
|
|
||||||
|
**High Memory Usage**
|
||||||
|
- Enable OPcache
|
||||||
|
- Increase PHP memory limit if needed
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
For troubleshooting, enable debug mode:
|
||||||
|
```bash
|
||||||
|
APP_DEBUG=true
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember to disable debug mode in production!
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
FROM php:8.4-cli
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
ENV APP_ENV=production \
|
||||||
|
APP_DEBUG=false \
|
||||||
|
PHP_OPCACHE_ENABLE=1 \
|
||||||
|
PHP_OPCACHE_VALIDATE_TIMESTAMPS=0 \
|
||||||
|
PHP_OPCACHE_MAX_ACCELERATED_FILES=10000 \
|
||||||
|
PHP_OPCACHE_MEMORY_CONSUMPTION=128 \
|
||||||
|
PORT=8080
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
bash \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
unzip \
|
||||||
|
pwgen \
|
||||||
|
xkcdpass \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
COPY . /var/www/html/
|
||||||
|
|
||||||
|
RUN composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist \
|
||||||
|
&& mkdir -p logs \
|
||||||
|
&& chmod 755 logs \
|
||||||
|
&& cp -n .env.example .env \
|
||||||
|
&& chown -R www-data:www-data /var/www/html
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||||
|
CMD curl -fsS http://127.0.0.1:8080/ >/dev/null || exit 1
|
||||||
|
|
||||||
|
CMD ["php", "-S", "0.0.0.0:8080", "-t", "public"]
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
# Password Generator API and Web App
|
||||||
|
|
||||||
|
A modern, secure password generator with a public REST API and multilingual web UI.
|
||||||
|
The project is built with Slim PHP and focuses on production-ready password generation.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Password Generators
|
||||||
|
|
||||||
|
- High Entropy: cryptographically secure random passwords with configurable character sets.
|
||||||
|
- XKCD Passphrase: memorable multi-word passphrases with language-specific wordlists.
|
||||||
|
- Diceware: dice-style passphrases with security metadata.
|
||||||
|
- PWGen Enhanced: pronounceable passwords with optional separator logic.
|
||||||
|
- Passkey: Apple-style pronounceable strings using syllable patterns.
|
||||||
|
|
||||||
|
### Product Capabilities
|
||||||
|
|
||||||
|
- Public JSON API for password generation.
|
||||||
|
- Responsive browser UI.
|
||||||
|
- Localization support with runtime locale endpoint.
|
||||||
|
- Security controls (rate limiting, CORS, secure random generation).
|
||||||
|
- Generated OpenAPI contract.
|
||||||
|
|
||||||
|
## Supported Languages
|
||||||
|
|
||||||
|
- en
|
||||||
|
- de
|
||||||
|
- de_CH
|
||||||
|
- fr
|
||||||
|
- it
|
||||||
|
- es
|
||||||
|
- pt
|
||||||
|
- ja
|
||||||
|
- zh
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- PHP 8.1+
|
||||||
|
- Composer
|
||||||
|
- Optional CLI tools for enhanced generator behavior: pwgen, xkcdpass
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd password-generator
|
||||||
|
composer install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
cp config/config.example.json config/config.json
|
||||||
|
mkdir -p logs
|
||||||
|
chmod 755 logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer start
|
||||||
|
# or
|
||||||
|
php -S localhost:8080 -t public
|
||||||
|
```
|
||||||
|
|
||||||
|
Open:
|
||||||
|
|
||||||
|
- [http://localhost:8080](http://localhost:8080)
|
||||||
|
- [http://localhost:8080/docs](http://localhost:8080/docs)
|
||||||
|
|
||||||
|
## API Usage
|
||||||
|
|
||||||
|
### List available generator types
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/api/v1/passwords
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generate passwords
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/v1/passwords/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"type":"diceware","count":2,"options":{"words":6}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get locale payload
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/api/v1/locales/ja
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generator Option Examples
|
||||||
|
|
||||||
|
### High Entropy
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "high-entropy",
|
||||||
|
"count": 1,
|
||||||
|
"options": {
|
||||||
|
"length": 16,
|
||||||
|
"include_lowercase": true,
|
||||||
|
"include_uppercase": true,
|
||||||
|
"include_numbers": true,
|
||||||
|
"include_symbols": true,
|
||||||
|
"safe_symbols_only": false,
|
||||||
|
"exclude_ambiguous": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### XKCD Passphrase
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "xkcdpass",
|
||||||
|
"count": 1,
|
||||||
|
"options": {
|
||||||
|
"words": 4,
|
||||||
|
"wordlist": "en",
|
||||||
|
"separator": "-",
|
||||||
|
"max_word_length": 0,
|
||||||
|
"space_handling": "allow",
|
||||||
|
"add_number": true,
|
||||||
|
"number_position": "end"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Diceware
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "diceware",
|
||||||
|
"count": 1,
|
||||||
|
"options": {
|
||||||
|
"words": 6,
|
||||||
|
"show_dice_rolls": true,
|
||||||
|
"uppercase_first": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PWGen Enhanced
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "pwgen",
|
||||||
|
"count": 1,
|
||||||
|
"options": {
|
||||||
|
"length": 16,
|
||||||
|
"separator": "-_",
|
||||||
|
"include_numbers": true,
|
||||||
|
"include_capitals": true,
|
||||||
|
"secure_mode": false,
|
||||||
|
"no_ambiguous": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Passkey
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "passkey",
|
||||||
|
"count": 1,
|
||||||
|
"options": {
|
||||||
|
"syllables": 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Notes
|
||||||
|
|
||||||
|
Primary runtime configuration files:
|
||||||
|
|
||||||
|
- config/config.json
|
||||||
|
- .env
|
||||||
|
|
||||||
|
Useful environment overrides:
|
||||||
|
|
||||||
|
- LLM_DEFAULT_PROVIDER
|
||||||
|
- OPENAI_API_KEY
|
||||||
|
- CLAUDE_API_KEY
|
||||||
|
- GEMINI_API_KEY
|
||||||
|
- PERPLEXITY_API_KEY
|
||||||
|
|
||||||
|
## OpenAPI Contract
|
||||||
|
|
||||||
|
Regenerate the public API specification when routes or response schemas change:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer generate:openapi
|
||||||
|
# or
|
||||||
|
php utils/generate-openapi.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Generated file:
|
||||||
|
|
||||||
|
- public/pwgenerator-swagger-spec.json
|
||||||
|
|
||||||
|
## Docker Deployment
|
||||||
|
|
||||||
|
### Build image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t password-generator:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -p 8080:8080 --name password-generator password-generator:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose (project-local setup)
|
||||||
|
|
||||||
|
The repository includes a project-specific Compose file for local development and testing.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
cp config/config.example.json config/config.json
|
||||||
|
mkdir -p logs
|
||||||
|
|
||||||
|
docker compose up --build -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open:
|
||||||
|
|
||||||
|
- [http://localhost:8080](http://localhost:8080)
|
||||||
|
- [http://localhost:8080/docs](http://localhost:8080/docs)
|
||||||
|
|
||||||
|
Useful commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose logs -f password-generator
|
||||||
|
docker compose restart password-generator
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Hub publish
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker login
|
||||||
|
docker tag password-generator:latest YOUR_DOCKERHUB_USERNAME/password-generator:latest
|
||||||
|
docker push YOUR_DOCKERHUB_USERNAME/password-generator:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
public/ - web root and generated OpenAPI spec
|
||||||
|
src/ - API, generators, middleware, and utilities
|
||||||
|
config/ - route, dependency, and app configuration
|
||||||
|
assets/ - frontend CSS and JavaScript
|
||||||
|
templates/ - HTML templates
|
||||||
|
locales/ - translation files
|
||||||
|
data/ - source wordlists
|
||||||
|
tests/ - PHPUnit tests
|
||||||
|
```
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/* Enhanced styling for detailed documentation */
|
||||||
|
.option-detail {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-left: 4px solid #007bff;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-detail h4 {
|
||||||
|
color: #007bff;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-detail .option-type {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-detail .option-default {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-request,
|
||||||
|
.example-response {
|
||||||
|
background: #2d3748;
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generator-section {
|
||||||
|
background: white;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generator-header {
|
||||||
|
background: #778491;
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generator-content {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector select {
|
||||||
|
border: none;
|
||||||
|
padding: 10px 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-note {
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-note::before {
|
||||||
|
content: "🔒 ";
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
+1319
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "drei/password-generator-api",
|
||||||
|
"description": "Secure password generator with REST API and web frontend (i18n-enabled)",
|
||||||
|
"type": "project",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.1",
|
||||||
|
"slim/slim": "^4.12",
|
||||||
|
"slim/psr7": "^1.6",
|
||||||
|
"psr/http-message": "^1.0",
|
||||||
|
"psr/log": "^3.0",
|
||||||
|
"monolog/monolog": "^3.0",
|
||||||
|
"vlucas/phpdotenv": "^5.5",
|
||||||
|
"tuupola/cors-middleware": "^1.4",
|
||||||
|
"php-di/php-di": "^7.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^10.0"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": ["Composer\\Config::disableProcessTimeout","php -S localhost:8080 -t public"],
|
||||||
|
"test": "phpunit",
|
||||||
|
"generate:openapi": "php utils/generate-openapi.php"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+3345
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,133 @@
|
|||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"name": "Password Generator",
|
||||||
|
"version": "4.1.0",
|
||||||
|
"environment": "production"
|
||||||
|
},
|
||||||
|
"locale": {
|
||||||
|
"default": "en",
|
||||||
|
"supported": [
|
||||||
|
"en",
|
||||||
|
"de",
|
||||||
|
"de_CH",
|
||||||
|
"fr",
|
||||||
|
"it",
|
||||||
|
"es",
|
||||||
|
"pt",
|
||||||
|
"ja",
|
||||||
|
"zh"
|
||||||
|
],
|
||||||
|
"fallback": "en",
|
||||||
|
"languages": {
|
||||||
|
"en": "English",
|
||||||
|
"de": "Deutsch",
|
||||||
|
"de_CH": "Schwyzerdütsch",
|
||||||
|
"fr": "Français",
|
||||||
|
"it": "Italiano",
|
||||||
|
"es": "Español",
|
||||||
|
"pt": "Português",
|
||||||
|
"ja": "日本語",
|
||||||
|
"zh": "中文"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"llm": {
|
||||||
|
"default_provider": "openai",
|
||||||
|
"timeout": 30,
|
||||||
|
"max_tokens": 500,
|
||||||
|
"temperature": 0.7,
|
||||||
|
"retry_attempts": 3,
|
||||||
|
"retry_delay": 1000,
|
||||||
|
"api_keys": {
|
||||||
|
"openai": "sk-example-openai-key",
|
||||||
|
"claude": "sk-ant-example-claude-key",
|
||||||
|
"gemini": "example-gemini-api-key",
|
||||||
|
"perplexity": "pplx-example-perplexity-key"
|
||||||
|
},
|
||||||
|
"providers": {
|
||||||
|
"openai": {
|
||||||
|
"name": "OpenAI",
|
||||||
|
"models": {
|
||||||
|
"gpt-3.5-turbo": "GPT-3.5 Turbo",
|
||||||
|
"gpt-4": "GPT-4",
|
||||||
|
"gpt-4-turbo": "GPT-4 Turbo",
|
||||||
|
"gpt-4o": "GPT-4 Omni"
|
||||||
|
},
|
||||||
|
"default_model": "gpt-3.5-turbo"
|
||||||
|
},
|
||||||
|
"claude": {
|
||||||
|
"name": "Claude",
|
||||||
|
"models": {
|
||||||
|
"claude-3-opus-20240229": "Claude 3 Opus",
|
||||||
|
"claude-3-sonnet-20240229": "Claude 3 Sonnet",
|
||||||
|
"claude-3-haiku-20240307": "Claude 3 Haiku"
|
||||||
|
},
|
||||||
|
"default_model": "claude-3-sonnet-20240229"
|
||||||
|
},
|
||||||
|
"gemini": {
|
||||||
|
"name": "Gemini",
|
||||||
|
"models": {
|
||||||
|
"gemini-pro": "Gemini Pro",
|
||||||
|
"gemini-1.5-pro": "Gemini 1.5 Pro",
|
||||||
|
"gemini-1.5-flash": "Gemini 1.5 Flash"
|
||||||
|
},
|
||||||
|
"default_model": "gemini-pro"
|
||||||
|
},
|
||||||
|
"perplexity": {
|
||||||
|
"name": "Perplexity",
|
||||||
|
"models": {
|
||||||
|
"llama-3-sonar-small-32k-online": "Llama 3 Sonar Small (32k)",
|
||||||
|
"llama-3-sonar-large-32k-online": "Llama 3 Sonar Large (32k)",
|
||||||
|
"llama-3-8b-instruct": "Llama 3 8B Instruct",
|
||||||
|
"llama-3-70b-instruct": "Llama 3 70B Instruct"
|
||||||
|
},
|
||||||
|
"default_model": "llama-3-sonar-small-32k-online"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"generator": {
|
||||||
|
"max_passwords_per_request": 100,
|
||||||
|
"default_count": 1,
|
||||||
|
"roulette_interval_seconds": 3,
|
||||||
|
"xkcd": {
|
||||||
|
"wordlists_path": "data/wordlists",
|
||||||
|
"wordlists": {
|
||||||
|
"en": {
|
||||||
|
"label": "EFF Large Wordlist (English)",
|
||||||
|
"file": "example_en_wordlist.txt"
|
||||||
|
},
|
||||||
|
"de": {
|
||||||
|
"label": "German Common Words",
|
||||||
|
"file": "example_de_wordlist.txt"
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
"label": "French Common Words",
|
||||||
|
"file": "example_fr_wordlist.txt"
|
||||||
|
},
|
||||||
|
"it": {
|
||||||
|
"label": "Italian Common Words",
|
||||||
|
"file": "example_it_wordlist.txt"
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"label": "Spanish Common Words",
|
||||||
|
"file": "example_es_wordlist.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cache_enabled": true,
|
||||||
|
"cache_ttl": 3600
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"rate_limit_enabled": true,
|
||||||
|
"rate_limit_max_requests": 120,
|
||||||
|
"rate_limit_window": 3600,
|
||||||
|
"require_https": true,
|
||||||
|
"cors_enabled": true,
|
||||||
|
"cors_origins": [
|
||||||
|
"https://yourdomain.com"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"logging": {
|
||||||
|
"enabled": true,
|
||||||
|
"level": "info"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"name": "Password Generator",
|
||||||
|
"version": "4.1.0",
|
||||||
|
"environment": "production"
|
||||||
|
},
|
||||||
|
"locale": {
|
||||||
|
"default": "en",
|
||||||
|
"supported": [
|
||||||
|
"en",
|
||||||
|
"de",
|
||||||
|
"de_CH",
|
||||||
|
"fr",
|
||||||
|
"it",
|
||||||
|
"es",
|
||||||
|
"pt",
|
||||||
|
"ja",
|
||||||
|
"zh"
|
||||||
|
],
|
||||||
|
"fallback": "en",
|
||||||
|
"languages": {
|
||||||
|
"en": "English",
|
||||||
|
"de": "Deutsch",
|
||||||
|
"de_CH": "Schwyzerdütsch",
|
||||||
|
"fr": "Français",
|
||||||
|
"it": "Italiano",
|
||||||
|
"es": "Español",
|
||||||
|
"pt": "Português",
|
||||||
|
"ja": "日本語",
|
||||||
|
"zh": "中文"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"llm": {
|
||||||
|
"default_provider": "openai",
|
||||||
|
"timeout": 30,
|
||||||
|
"max_tokens": 500,
|
||||||
|
"temperature": 0.7,
|
||||||
|
"retry_attempts": 3,
|
||||||
|
"retry_delay": 1000,
|
||||||
|
"providers": {
|
||||||
|
"openai": {
|
||||||
|
"name": "OpenAI",
|
||||||
|
"models": {
|
||||||
|
"gpt-3.5-turbo": "GPT-3.5 Turbo",
|
||||||
|
"gpt-4": "GPT-4",
|
||||||
|
"gpt-4-turbo": "GPT-4 Turbo",
|
||||||
|
"gpt-4o": "GPT-4 Omni"
|
||||||
|
},
|
||||||
|
"default_model": "gpt-3.5-turbo"
|
||||||
|
},
|
||||||
|
"claude": {
|
||||||
|
"name": "Claude",
|
||||||
|
"models": {
|
||||||
|
"claude-3-opus-20240229": "Claude 3 Opus",
|
||||||
|
"claude-3-sonnet-20240229": "Claude 3 Sonnet",
|
||||||
|
"claude-3-haiku-20240307": "Claude 3 Haiku"
|
||||||
|
},
|
||||||
|
"default_model": "claude-3-sonnet-20240229"
|
||||||
|
},
|
||||||
|
"gemini": {
|
||||||
|
"name": "Gemini",
|
||||||
|
"models": {
|
||||||
|
"gemini-pro": "Gemini Pro",
|
||||||
|
"gemini-1.5-pro": "Gemini 1.5 Pro",
|
||||||
|
"gemini-1.5-flash": "Gemini 1.5 Flash"
|
||||||
|
},
|
||||||
|
"default_model": "gemini-pro"
|
||||||
|
},
|
||||||
|
"perplexity": {
|
||||||
|
"name": "Perplexity",
|
||||||
|
"models": {
|
||||||
|
"llama-3-sonar-small-32k-online": "Llama 3 Sonar Small (32k)",
|
||||||
|
"llama-3-sonar-large-32k-online": "Llama 3 Sonar Large (32k)",
|
||||||
|
"llama-3-8b-instruct": "Llama 3 8B Instruct",
|
||||||
|
"llama-3-70b-instruct": "Llama 3 70B Instruct"
|
||||||
|
},
|
||||||
|
"default_model": "llama-3-sonar-small-32k-online"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"generator": {
|
||||||
|
"max_passwords_per_request": 100,
|
||||||
|
"default_count": 1,
|
||||||
|
"roulette_interval_seconds": 3,
|
||||||
|
"xkcd": {
|
||||||
|
"wordlists_path": "data/wordlists",
|
||||||
|
"wordlists": {
|
||||||
|
"en": {
|
||||||
|
"label": "EFF English Wordlist",
|
||||||
|
"file": "eff_large_wordlist_en.txt"
|
||||||
|
},
|
||||||
|
"de": {
|
||||||
|
"label": "German Common Words",
|
||||||
|
"file": "german_common_words_de.txt"
|
||||||
|
},
|
||||||
|
"ch": {
|
||||||
|
"label": "Slängikon",
|
||||||
|
"file": "swiss_german_slaengikon.txt"
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
"label": "French Common Words",
|
||||||
|
"file": "french_common_words_fr.txt"
|
||||||
|
},
|
||||||
|
"it": {
|
||||||
|
"label": "Italian Common Words",
|
||||||
|
"file": "italian_common_words_it.txt"
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"label": "Spanish Common Words",
|
||||||
|
"file": "spanish_common_words_es.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cache_enabled": true,
|
||||||
|
"cache_ttl": 3600
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"rate_limit_enabled": true,
|
||||||
|
"rate_limit_max_requests": 120,
|
||||||
|
"rate_limit_window": 3600,
|
||||||
|
"require_https": true,
|
||||||
|
"cors_enabled": true,
|
||||||
|
"cors_origins": [
|
||||||
|
"https://yourdomain.com"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"logging": {
|
||||||
|
"enabled": true,
|
||||||
|
"level": "info"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Api\PasswordController;
|
||||||
|
use App\Managers\PasswordGeneratorManager;
|
||||||
|
use App\Utils\ResponseHelper;
|
||||||
|
use App\Utils\SecurityConfig;
|
||||||
|
use DI\ContainerBuilder;
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
|
use Monolog\Logger;
|
||||||
|
use Monolog\Processor\UidProcessor;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
return function (ContainerBuilder $containerBuilder) {
|
||||||
|
$containerBuilder->addDefinitions([
|
||||||
|
LoggerInterface::class => function (ContainerInterface $c) {
|
||||||
|
$settings = [
|
||||||
|
'name' => $_ENV['APP_NAME'] ?? 'password-generator',
|
||||||
|
'path' => __DIR__ . '/../' . ($_ENV['LOG_PATH'] ?? 'logs/app.log'),
|
||||||
|
'level' => SecurityConfig::resolveLogLevel($_ENV['LOG_LEVEL'] ?? 'warning'),
|
||||||
|
];
|
||||||
|
|
||||||
|
$logger = new Logger($settings['name']);
|
||||||
|
|
||||||
|
$processor = new UidProcessor();
|
||||||
|
$logger->pushProcessor($processor);
|
||||||
|
|
||||||
|
$handler = new StreamHandler($settings['path'], $settings['level']);
|
||||||
|
$logger->pushHandler($handler);
|
||||||
|
|
||||||
|
return $logger;
|
||||||
|
},
|
||||||
|
|
||||||
|
ResponseHelper::class => function (ContainerInterface $c) {
|
||||||
|
return new ResponseHelper();
|
||||||
|
},
|
||||||
|
|
||||||
|
PasswordGeneratorManager::class => function (ContainerInterface $c) {
|
||||||
|
return new PasswordGeneratorManager();
|
||||||
|
},
|
||||||
|
|
||||||
|
PasswordController::class => function (ContainerInterface $c) {
|
||||||
|
return new PasswordController(
|
||||||
|
$c->get(PasswordGeneratorManager::class),
|
||||||
|
$c->get(ResponseHelper::class),
|
||||||
|
$c->get(LoggerInterface::class)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Middleware\CorsMiddleware;
|
||||||
|
use App\Middleware\JsonResponseMiddleware;
|
||||||
|
use App\Middleware\RateLimitMiddleware;
|
||||||
|
use Slim\App;
|
||||||
|
use Slim\Middleware\ErrorMiddleware;
|
||||||
|
|
||||||
|
return function (App $app) {
|
||||||
|
// Parse JSON, form data and XML
|
||||||
|
$app->addBodyParsingMiddleware();
|
||||||
|
|
||||||
|
// Add CORS middleware
|
||||||
|
$app->add(new CorsMiddleware());
|
||||||
|
|
||||||
|
// Add JSON response middleware
|
||||||
|
$app->add(new JsonResponseMiddleware());
|
||||||
|
|
||||||
|
// Add rate limiting middleware
|
||||||
|
$app->add(new RateLimitMiddleware());
|
||||||
|
|
||||||
|
// Add routing middleware
|
||||||
|
$app->addRoutingMiddleware();
|
||||||
|
|
||||||
|
// Add error middleware
|
||||||
|
$displayErrorDetails = filter_var($_ENV['APP_DEBUG'] ?? 'false', FILTER_VALIDATE_BOOLEAN);
|
||||||
|
$logErrors = true;
|
||||||
|
$logErrorDetails = $displayErrorDetails;
|
||||||
|
|
||||||
|
$errorMiddleware = $app->addErrorMiddleware($displayErrorDetails, $logErrors, $logErrorDetails);
|
||||||
|
|
||||||
|
return $app;
|
||||||
|
};
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Api\PasswordController;
|
||||||
|
use Slim\App;
|
||||||
|
use Slim\Interfaces\RouteCollectorProxyInterface as Group;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
|
||||||
|
return function (App $app) {
|
||||||
|
// Serve static frontend
|
||||||
|
$app->get('/', function (Request $request, Response $response, $args) {
|
||||||
|
$html = file_get_contents(__DIR__ . '/../templates/index.html');
|
||||||
|
$response->getBody()->write($html);
|
||||||
|
return $response->withHeader('Content-Type', 'text/html');
|
||||||
|
});
|
||||||
|
|
||||||
|
// API routes
|
||||||
|
$app->group('/api/v1', function (Group $group) {
|
||||||
|
// Password generation endpoints
|
||||||
|
$group->get('/passwords', PasswordController::class . ':getPasswordTypes');
|
||||||
|
$group->get('/passwords/generators', PasswordController::class . ':getGenerators');
|
||||||
|
$group->post('/passwords/generate', PasswordController::class . ':generatePasswords');
|
||||||
|
|
||||||
|
// NEW: Available wordlists endpoint for dynamic language dropdown
|
||||||
|
$group->get('/passwords/wordlists', PasswordController::class . ':getAvailableWordlists');
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
$group->get('/health', function (Request $request, Response $response) {
|
||||||
|
$response->getBody()->write(json_encode([
|
||||||
|
'status' => 'healthy',
|
||||||
|
'version' => $_ENV['APP_VERSION'] ?? '1.0.0',
|
||||||
|
'timestamp' => date('c')
|
||||||
|
]));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Updated locales endpoint with fixed pattern validation
|
||||||
|
$group->get('/locales/{lang:[A-Za-z_]+}', function (Request $request, Response $response, array $args) {
|
||||||
|
$lang = $args['lang'];
|
||||||
|
|
||||||
|
// Updated supported languages list to match frontend and wordlists
|
||||||
|
$supportedLangs = ['en', 'de', 'de_CH', 'fr', 'it', 'es', 'pt', 'ja', 'zh'];
|
||||||
|
|
||||||
|
if (!in_array($lang, $supportedLangs, true)) {
|
||||||
|
$lang = 'en'; // fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve Swiss German locale when available, otherwise fall back to German
|
||||||
|
if ($lang === 'ch') {
|
||||||
|
$lang = 'de';
|
||||||
|
}
|
||||||
|
if ($lang === 'de_CH' && !file_exists(__DIR__ . '/../locales/de_CH.json')) {
|
||||||
|
$lang = 'de';
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = __DIR__ . "/../locales/{$lang}.json";
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
return $response->withStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($path);
|
||||||
|
$response->getBody()->write($content);
|
||||||
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
// API documentation
|
||||||
|
$app->get('/docs', function (Request $request, Response $response, $args) {
|
||||||
|
$html = file_get_contents(__DIR__ . '/../templates/docs.html');
|
||||||
|
$response->getBody()->write($html);
|
||||||
|
return $response->withHeader('Content-Type', 'text/html');
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Serve static assets
|
||||||
|
$app->get('/assets/{type}/{file}', function (Request $request, Response $response, array $args) {
|
||||||
|
$type = $args['type'];
|
||||||
|
$file = $args['file'];
|
||||||
|
$path = __DIR__ . "/../assets/{$type}/{$file}";
|
||||||
|
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
return $response->withStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($path);
|
||||||
|
$contentType = $type === 'css' ? 'text/css' : 'application/javascript';
|
||||||
|
|
||||||
|
$response->getBody()->write($content);
|
||||||
|
return $response->withHeader('Content-Type', $contentType);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Serve locale files directly with improved pattern matching
|
||||||
|
$app->get('/locales/{file:[A-Za-z_]+\.json}', function (Request $request, Response $response, array $args) {
|
||||||
|
$file = $args['file'];
|
||||||
|
$path = __DIR__ . "/../locales/{$file}";
|
||||||
|
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
return $response->withStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($path);
|
||||||
|
$response->getBody()->write($content);
|
||||||
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Main documentation route (English - default)
|
||||||
|
$app->get('/docs', function ($request, $response) {
|
||||||
|
$docFile = __DIR__ . '/../docs/index.html';
|
||||||
|
|
||||||
|
if (file_exists($docFile)) {
|
||||||
|
$response->getBody()->write(file_get_contents($docFile));
|
||||||
|
return $response->withHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
|
} else {
|
||||||
|
return $response->withStatus(404)->write('Documentation not found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Language-specific documentation routes
|
||||||
|
$app->get('/docs-{lang}', function ($request, $response, array $args) {
|
||||||
|
$lang = $args['lang'];
|
||||||
|
|
||||||
|
// Allowed languages
|
||||||
|
$allowedLanguages = ['de', 'fr', 'es', 'it', 'ja', 'zh'];
|
||||||
|
|
||||||
|
if (!in_array($lang, $allowedLanguages)) {
|
||||||
|
return $response->withStatus(302)->withHeader('Location', '/docs');
|
||||||
|
}
|
||||||
|
|
||||||
|
$docFile = __DIR__ . "/../docs/docs-{$lang}.html";
|
||||||
|
|
||||||
|
if (file_exists($docFile)) {
|
||||||
|
$response->getBody()->write(file_get_contents($docFile));
|
||||||
|
return $response->withHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
|
} else {
|
||||||
|
// Fallback to English
|
||||||
|
$fallbackFile = __DIR__ . '/../docs/index.html';
|
||||||
|
if (file_exists($fallbackFile)) {
|
||||||
|
$response->getBody()->write(file_get_contents($fallbackFile));
|
||||||
|
return $response->withHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
|
} else {
|
||||||
|
return $response->withStatus(404)->write('Documentation not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Static assets for documentation
|
||||||
|
$app->get('/docs/assets/{type}/{file}', function ($request, $response, array $args) {
|
||||||
|
$type = $args['type'];
|
||||||
|
$file = $args['file'];
|
||||||
|
|
||||||
|
// Security: prevent directory traversal
|
||||||
|
if (strpos($file, '..') !== false || strpos($type, '..') !== false) {
|
||||||
|
return $response->withStatus(403)->write('Access denied');
|
||||||
|
}
|
||||||
|
|
||||||
|
$assetFile = __DIR__ . "/../docs/assets/{$type}/{$file}";
|
||||||
|
|
||||||
|
if (file_exists($assetFile)) {
|
||||||
|
$mimeTypes = [
|
||||||
|
'css' => 'text/css',
|
||||||
|
'js' => 'application/javascript',
|
||||||
|
'png' => 'image/png',
|
||||||
|
'jpg' => 'image/jpeg',
|
||||||
|
'svg' => 'image/svg+xml'
|
||||||
|
];
|
||||||
|
|
||||||
|
$extension = pathinfo($file, PATHINFO_EXTENSION);
|
||||||
|
$mimeType = $mimeTypes[$extension] ?? 'text/plain';
|
||||||
|
|
||||||
|
$response->getBody()->write(file_get_contents($assetFile));
|
||||||
|
return $response->withHeader('Content-Type', $mimeType);
|
||||||
|
} else {
|
||||||
|
return $response->withStatus(404)->write('Asset not found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Password Generator Package Creator
|
||||||
|
# Creates a distributable archive of the complete i18n-enabled password generator
|
||||||
|
|
||||||
|
echo "📦 Creating Password Generator Distribution Package"
|
||||||
|
echo "================================================="
|
||||||
|
|
||||||
|
# Get version from environment or use default
|
||||||
|
VERSION=${1:-"2.0.0-i18n"}
|
||||||
|
PACKAGE_NAME="password-generator-${VERSION}"
|
||||||
|
|
||||||
|
# Create temporary directory structure
|
||||||
|
echo "📁 Preparing package structure..."
|
||||||
|
rm -rf dist/
|
||||||
|
mkdir -p dist/${PACKAGE_NAME}
|
||||||
|
|
||||||
|
# Copy all necessary files
|
||||||
|
cp -r config/ dist/${PACKAGE_NAME}/
|
||||||
|
cp -r src/ dist/${PACKAGE_NAME}/
|
||||||
|
cp -r templates/ dist/${PACKAGE_NAME}/
|
||||||
|
cp -r assets/ dist/${PACKAGE_NAME}/
|
||||||
|
cp -r locales/ dist/${PACKAGE_NAME}/
|
||||||
|
cp -r public/ dist/${PACKAGE_NAME}/
|
||||||
|
|
||||||
|
# Copy configuration and documentation
|
||||||
|
cp composer.json dist/${PACKAGE_NAME}/
|
||||||
|
cp .env dist/${PACKAGE_NAME}/.env.example
|
||||||
|
cp README.md dist/${PACKAGE_NAME}/
|
||||||
|
cp DEPLOYMENT.md dist/${PACKAGE_NAME}/
|
||||||
|
cp install.sh dist/${PACKAGE_NAME}/
|
||||||
|
|
||||||
|
# Create logs directory with placeholder
|
||||||
|
mkdir -p dist/${PACKAGE_NAME}/logs
|
||||||
|
cp logs/README.md dist/${PACKAGE_NAME}/logs/
|
||||||
|
|
||||||
|
# Create archive
|
||||||
|
cd dist/
|
||||||
|
echo "🗜️ Creating ZIP archive..."
|
||||||
|
zip -r ${PACKAGE_NAME}.zip ${PACKAGE_NAME}/ -x "*.DS_Store" "*.git*" "vendor/*" "node_modules/*"
|
||||||
|
|
||||||
|
# Create tar.gz archive
|
||||||
|
echo "🗜️ Creating TAR.GZ archive..."
|
||||||
|
tar -czf ${PACKAGE_NAME}.tar.gz ${PACKAGE_NAME}/
|
||||||
|
|
||||||
|
# Calculate checksums
|
||||||
|
echo "🔐 Calculating checksums..."
|
||||||
|
sha256sum ${PACKAGE_NAME}.zip > ${PACKAGE_NAME}.zip.sha256
|
||||||
|
sha256sum ${PACKAGE_NAME}.tar.gz > ${PACKAGE_NAME}.tar.gz.sha256
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Display results
|
||||||
|
echo ""
|
||||||
|
echo "✅ Package creation completed!"
|
||||||
|
echo ""
|
||||||
|
echo "📁 Generated files:"
|
||||||
|
ls -la dist/
|
||||||
|
echo ""
|
||||||
|
echo "📊 Package statistics:"
|
||||||
|
echo "Files: $(find dist/${PACKAGE_NAME}/ -type f | wc -l)"
|
||||||
|
echo "Directories: $(find dist/${PACKAGE_NAME}/ -type d | wc -l)"
|
||||||
|
echo "ZIP size: $(du -h dist/${PACKAGE_NAME}.zip | cut -f1)"
|
||||||
|
echo "TAR.GZ size: $(du -h dist/${PACKAGE_NAME}.tar.gz | cut -f1)"
|
||||||
|
echo ""
|
||||||
|
echo "🚀 Ready for distribution!"
|
||||||
|
echo "Share the files in dist/ directory"
|
||||||
|
echo ""
|
||||||
|
echo "📋 Installation instructions for end users:"
|
||||||
|
echo "1. Extract the archive"
|
||||||
|
echo "2. cd ${PACKAGE_NAME}/"
|
||||||
|
echo "3. chmod +x install.sh && ./install.sh"
|
||||||
|
echo "4. composer start"
|
||||||
|
echo "5. Visit: http://localhost:8080"
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,168 @@
|
|||||||
|
# French Common Words - Wordlist
|
||||||
|
# Mots français courants pour phrases de passe sécurisées
|
||||||
|
|
||||||
|
avec
|
||||||
|
être
|
||||||
|
avoir
|
||||||
|
faire
|
||||||
|
tout
|
||||||
|
aller
|
||||||
|
voir
|
||||||
|
savoir
|
||||||
|
pouvoir
|
||||||
|
falloir
|
||||||
|
vouloir
|
||||||
|
venir
|
||||||
|
dire
|
||||||
|
prendre
|
||||||
|
donner
|
||||||
|
mettre
|
||||||
|
partir
|
||||||
|
parler
|
||||||
|
finir
|
||||||
|
arriver
|
||||||
|
rester
|
||||||
|
entendre
|
||||||
|
demander
|
||||||
|
monter
|
||||||
|
passer
|
||||||
|
regarder
|
||||||
|
aimer
|
||||||
|
porter
|
||||||
|
appeler
|
||||||
|
ouvrir
|
||||||
|
temps
|
||||||
|
personne
|
||||||
|
année
|
||||||
|
façon
|
||||||
|
jour
|
||||||
|
homme
|
||||||
|
chose
|
||||||
|
enfant
|
||||||
|
pays
|
||||||
|
problème
|
||||||
|
monde
|
||||||
|
vie
|
||||||
|
main
|
||||||
|
famille
|
||||||
|
groupe
|
||||||
|
moment
|
||||||
|
question
|
||||||
|
droit
|
||||||
|
service
|
||||||
|
parti
|
||||||
|
nombre
|
||||||
|
maison
|
||||||
|
côté
|
||||||
|
travail
|
||||||
|
place
|
||||||
|
heure
|
||||||
|
état
|
||||||
|
pouvoir
|
||||||
|
part
|
||||||
|
moins
|
||||||
|
ville
|
||||||
|
grand
|
||||||
|
petit
|
||||||
|
premier
|
||||||
|
dernier
|
||||||
|
eau
|
||||||
|
grande
|
||||||
|
livre
|
||||||
|
nouveau
|
||||||
|
porter
|
||||||
|
raison
|
||||||
|
fois
|
||||||
|
tête
|
||||||
|
avant
|
||||||
|
après
|
||||||
|
france
|
||||||
|
français
|
||||||
|
politique
|
||||||
|
français
|
||||||
|
national
|
||||||
|
centre
|
||||||
|
euro
|
||||||
|
gouvernement
|
||||||
|
ministre
|
||||||
|
présent
|
||||||
|
conseil
|
||||||
|
paris
|
||||||
|
point
|
||||||
|
cours
|
||||||
|
formation
|
||||||
|
information
|
||||||
|
système
|
||||||
|
équipe
|
||||||
|
projet
|
||||||
|
entreprise
|
||||||
|
activité
|
||||||
|
recherche
|
||||||
|
résultat
|
||||||
|
programme
|
||||||
|
société
|
||||||
|
sécurité
|
||||||
|
communauté
|
||||||
|
développement
|
||||||
|
économie
|
||||||
|
marché
|
||||||
|
relation
|
||||||
|
culture
|
||||||
|
qualité
|
||||||
|
niveau
|
||||||
|
histoire
|
||||||
|
région
|
||||||
|
territoire
|
||||||
|
administration
|
||||||
|
communication
|
||||||
|
direction
|
||||||
|
mission
|
||||||
|
objectif
|
||||||
|
organisation
|
||||||
|
public
|
||||||
|
international
|
||||||
|
service
|
||||||
|
social
|
||||||
|
national
|
||||||
|
général
|
||||||
|
particulier
|
||||||
|
personnel
|
||||||
|
professionnel
|
||||||
|
politique
|
||||||
|
économique
|
||||||
|
public
|
||||||
|
social
|
||||||
|
européen
|
||||||
|
français
|
||||||
|
grand
|
||||||
|
nouveau
|
||||||
|
premier
|
||||||
|
dernier
|
||||||
|
important
|
||||||
|
possible
|
||||||
|
différent
|
||||||
|
général
|
||||||
|
local
|
||||||
|
national
|
||||||
|
international
|
||||||
|
européen
|
||||||
|
public
|
||||||
|
social
|
||||||
|
économique
|
||||||
|
politique
|
||||||
|
technique
|
||||||
|
militaire
|
||||||
|
culturel
|
||||||
|
naturel
|
||||||
|
humain
|
||||||
|
civil
|
||||||
|
commercial
|
||||||
|
industriel
|
||||||
|
financier
|
||||||
|
médical
|
||||||
|
scientifique
|
||||||
|
artistique
|
||||||
|
sportif
|
||||||
|
éducatif
|
||||||
|
alimentaire
|
||||||
|
environnemental
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
# German Common Words - Wordlist
|
||||||
|
# Häufige deutsche Wörter für sichere Passphrasen
|
||||||
|
|
||||||
|
aber
|
||||||
|
alle
|
||||||
|
alles
|
||||||
|
also
|
||||||
|
andere
|
||||||
|
Anfang
|
||||||
|
arbeiten
|
||||||
|
auch
|
||||||
|
auf
|
||||||
|
aus
|
||||||
|
bei
|
||||||
|
bekommen
|
||||||
|
bereits
|
||||||
|
bis
|
||||||
|
bleiben
|
||||||
|
dabei
|
||||||
|
damit
|
||||||
|
dann
|
||||||
|
der
|
||||||
|
deutsche
|
||||||
|
doch
|
||||||
|
drei
|
||||||
|
durch
|
||||||
|
eine
|
||||||
|
einem
|
||||||
|
einen
|
||||||
|
einer
|
||||||
|
eines
|
||||||
|
einige
|
||||||
|
einzelne
|
||||||
|
entsprechend
|
||||||
|
ersten
|
||||||
|
etwa
|
||||||
|
etwas
|
||||||
|
Euro
|
||||||
|
finden
|
||||||
|
Frage
|
||||||
|
Fragen
|
||||||
|
gegen
|
||||||
|
gehen
|
||||||
|
gibt
|
||||||
|
grosse
|
||||||
|
grösste
|
||||||
|
gruppe
|
||||||
|
haben
|
||||||
|
Haus
|
||||||
|
heute
|
||||||
|
hier
|
||||||
|
hinter
|
||||||
|
jedoch
|
||||||
|
jetzt
|
||||||
|
kann
|
||||||
|
kommen
|
||||||
|
können
|
||||||
|
kurz
|
||||||
|
Land
|
||||||
|
lassen
|
||||||
|
laut
|
||||||
|
Leben
|
||||||
|
machen
|
||||||
|
mann
|
||||||
|
mehr
|
||||||
|
mein
|
||||||
|
Menschen
|
||||||
|
mit
|
||||||
|
nach
|
||||||
|
nachdem
|
||||||
|
name
|
||||||
|
nicht
|
||||||
|
noch
|
||||||
|
oder
|
||||||
|
ohne
|
||||||
|
platz
|
||||||
|
recht
|
||||||
|
sagen
|
||||||
|
schon
|
||||||
|
sehr
|
||||||
|
sein
|
||||||
|
sich
|
||||||
|
sie
|
||||||
|
sind
|
||||||
|
über
|
||||||
|
unter
|
||||||
|
verschiedene
|
||||||
|
viel
|
||||||
|
vom
|
||||||
|
vor
|
||||||
|
wann
|
||||||
|
was
|
||||||
|
weil
|
||||||
|
wasser
|
||||||
|
weg
|
||||||
|
welt
|
||||||
|
werden
|
||||||
|
wie
|
||||||
|
wieder
|
||||||
|
will
|
||||||
|
wir
|
||||||
|
wissen
|
||||||
|
wo
|
||||||
|
woche
|
||||||
|
wohl
|
||||||
|
während
|
||||||
|
zwischen
|
||||||
|
abenteuer
|
||||||
|
ablehnung
|
||||||
|
absicht
|
||||||
|
abteilung
|
||||||
|
achtung
|
||||||
|
aktivität
|
||||||
|
anfrage
|
||||||
|
angebot
|
||||||
|
ankunft
|
||||||
|
antwort
|
||||||
|
arbeit
|
||||||
|
aufgabe
|
||||||
|
aufmerksamkeit
|
||||||
|
auswahl
|
||||||
|
bedeutung
|
||||||
|
bedingung
|
||||||
|
behandlung
|
||||||
|
bereich
|
||||||
|
besuch
|
||||||
|
bewegung
|
||||||
|
bewertung
|
||||||
|
beziehung
|
||||||
|
bildung
|
||||||
|
chance
|
||||||
|
darstellung
|
||||||
|
denken
|
||||||
|
deutschland
|
||||||
|
entwicklung
|
||||||
|
erfahrung
|
||||||
|
erfolg
|
||||||
|
ergebnis
|
||||||
|
erhöhung
|
||||||
|
erinnerung
|
||||||
|
erklärung
|
||||||
|
erwartung
|
||||||
|
erzählung
|
||||||
|
familie
|
||||||
|
farbe
|
||||||
|
fehler
|
||||||
|
feier
|
||||||
|
fieber
|
||||||
|
fläche
|
||||||
|
form
|
||||||
|
frage
|
||||||
|
frau
|
||||||
|
freiheit
|
||||||
|
freude
|
||||||
|
freund
|
||||||
|
function
|
||||||
|
gefahr
|
||||||
|
gefühl
|
||||||
|
gegenwart
|
||||||
|
geld
|
||||||
|
gemeinde
|
||||||
|
geschichte
|
||||||
|
gesellschaft
|
||||||
|
gespräch
|
||||||
|
gewalt
|
||||||
|
gewicht
|
||||||
|
glaube
|
||||||
|
grund
|
||||||
|
gruppe
|
||||||
|
handel
|
||||||
|
haupt
|
||||||
|
herr
|
||||||
|
hoffnung
|
||||||
|
information
|
||||||
|
inhalt
|
||||||
|
interesse
|
||||||
|
jahr
|
||||||
|
jahrhundert
|
||||||
|
kind
|
||||||
|
kirche
|
||||||
|
klasse
|
||||||
|
kraft
|
||||||
|
krieg
|
||||||
|
kultur
|
||||||
|
kunst
|
||||||
|
leben
|
||||||
|
lehrer
|
||||||
|
leistung
|
||||||
|
Liebe
|
||||||
|
Licht
|
||||||
|
Luft
|
||||||
|
macht
|
||||||
|
mann
|
||||||
|
markt
|
||||||
|
masse
|
||||||
|
material
|
||||||
|
medizin
|
||||||
|
meinung
|
||||||
|
mensch
|
||||||
|
möglichkeit
|
||||||
|
moment
|
||||||
|
monat
|
||||||
|
musik
|
||||||
|
nacht
|
||||||
|
nachricht
|
||||||
|
natur
|
||||||
|
nummer
|
||||||
|
ort
|
||||||
|
person
|
||||||
|
plan
|
||||||
|
politik
|
||||||
|
position
|
||||||
|
preis
|
||||||
|
problem
|
||||||
|
prozent
|
||||||
|
prozess
|
||||||
|
punkt
|
||||||
|
raum
|
||||||
|
recht
|
||||||
|
region
|
||||||
|
regierung
|
||||||
|
reise
|
||||||
|
rolle
|
||||||
|
sache
|
||||||
|
schule
|
||||||
|
seite
|
||||||
|
situation
|
||||||
|
sommer
|
||||||
|
sprache
|
||||||
|
staat
|
||||||
|
stadt
|
||||||
|
stelle
|
||||||
|
stimme
|
||||||
|
stunde
|
||||||
|
system
|
||||||
|
tag
|
||||||
|
teil
|
||||||
|
text
|
||||||
|
thema
|
||||||
|
tisch
|
||||||
|
titel
|
||||||
|
tod
|
||||||
|
tradition
|
||||||
|
uhr
|
||||||
|
unternehmen
|
||||||
|
unterschied
|
||||||
|
ursache
|
||||||
|
veränderung
|
||||||
|
verhalten
|
||||||
|
verkehr
|
||||||
|
vermögen
|
||||||
|
versuch
|
||||||
|
vertrag
|
||||||
|
vertrauen
|
||||||
|
verwaltung
|
||||||
|
verwendung
|
||||||
|
volk
|
||||||
|
wahrheit
|
||||||
|
ware
|
||||||
|
wasser
|
||||||
|
weg
|
||||||
|
welt
|
||||||
|
wert
|
||||||
|
wirtschaft
|
||||||
|
wissenschaft
|
||||||
|
woche
|
||||||
|
wohnung
|
||||||
|
Wort
|
||||||
|
Zahl
|
||||||
|
Zeit
|
||||||
|
Zeitung
|
||||||
|
Ziel
|
||||||
|
Zukunft
|
||||||
|
Zusammenhang
|
||||||
|
Zustand
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
# Italian Common Words - Wordlist
|
||||||
|
# Parole italiane comuni per passphrase sicure
|
||||||
|
|
||||||
|
essere
|
||||||
|
avere
|
||||||
|
fare
|
||||||
|
dire
|
||||||
|
andare
|
||||||
|
potere
|
||||||
|
dovere
|
||||||
|
volere
|
||||||
|
sapere
|
||||||
|
dare
|
||||||
|
stare
|
||||||
|
vedere
|
||||||
|
uscire
|
||||||
|
partire
|
||||||
|
parlare
|
||||||
|
leggere
|
||||||
|
scrivere
|
||||||
|
sentire
|
||||||
|
mettere
|
||||||
|
portare
|
||||||
|
anno
|
||||||
|
giorno
|
||||||
|
tempo
|
||||||
|
volta
|
||||||
|
persona
|
||||||
|
modo
|
||||||
|
casa
|
||||||
|
parte
|
||||||
|
cosa
|
||||||
|
paese
|
||||||
|
problema
|
||||||
|
mondo
|
||||||
|
vita
|
||||||
|
mano
|
||||||
|
famiglia
|
||||||
|
gruppo
|
||||||
|
momento
|
||||||
|
domanda
|
||||||
|
diritto
|
||||||
|
servizio
|
||||||
|
lavoro
|
||||||
|
posto
|
||||||
|
ora
|
||||||
|
stato
|
||||||
|
governo
|
||||||
|
italia
|
||||||
|
italiano
|
||||||
|
nazionale
|
||||||
|
centro
|
||||||
|
euro
|
||||||
|
politica
|
||||||
|
ministro
|
||||||
|
presente
|
||||||
|
consiglio
|
||||||
|
roma
|
||||||
|
punto
|
||||||
|
corso
|
||||||
|
formazione
|
||||||
|
informazione
|
||||||
|
sistema
|
||||||
|
squadra
|
||||||
|
progetto
|
||||||
|
azienda
|
||||||
|
attività
|
||||||
|
ricerca
|
||||||
|
risultato
|
||||||
|
programma
|
||||||
|
società
|
||||||
|
sicurezza
|
||||||
|
comunità
|
||||||
|
sviluppo
|
||||||
|
economia
|
||||||
|
mercato
|
||||||
|
relazione
|
||||||
|
cultura
|
||||||
|
qualità
|
||||||
|
livello
|
||||||
|
storia
|
||||||
|
regione
|
||||||
|
territorio
|
||||||
|
amministrazione
|
||||||
|
comunicazione
|
||||||
|
direzione
|
||||||
|
missione
|
||||||
|
obiettivo
|
||||||
|
organizzazione
|
||||||
|
pubblico
|
||||||
|
internazionale
|
||||||
|
sociale
|
||||||
|
generale
|
||||||
|
particolare
|
||||||
|
personale
|
||||||
|
professionale
|
||||||
|
economico
|
||||||
|
europeo
|
||||||
|
grande
|
||||||
|
nuovo
|
||||||
|
primo
|
||||||
|
ultimo
|
||||||
|
importante
|
||||||
|
possibile
|
||||||
|
diverso
|
||||||
|
locale
|
||||||
|
militare
|
||||||
|
culturale
|
||||||
|
naturale
|
||||||
|
umano
|
||||||
|
civile
|
||||||
|
commerciale
|
||||||
|
industriale
|
||||||
|
finanziario
|
||||||
|
medico
|
||||||
|
scientifico
|
||||||
|
artistico
|
||||||
|
sportivo
|
||||||
|
educativo
|
||||||
|
alimentare
|
||||||
|
ambientale
|
||||||
|
bene
|
||||||
|
male
|
||||||
|
molto
|
||||||
|
poco
|
||||||
|
tutto
|
||||||
|
niente
|
||||||
|
sempre
|
||||||
|
mai
|
||||||
|
oggi
|
||||||
|
ieri
|
||||||
|
domani
|
||||||
|
ora
|
||||||
|
prima
|
||||||
|
dopo
|
||||||
|
qui
|
||||||
|
dove
|
||||||
|
come
|
||||||
|
perché
|
||||||
|
quando
|
||||||
|
quale
|
||||||
|
quanto
|
||||||
|
chi
|
||||||
|
cosa
|
||||||
|
sopra
|
||||||
|
sotto
|
||||||
|
dentro
|
||||||
|
fuori
|
||||||
|
davanti
|
||||||
|
dietro
|
||||||
|
prima
|
||||||
|
dopo
|
||||||
|
insieme
|
||||||
|
solo
|
||||||
|
anche
|
||||||
|
ancora
|
||||||
|
già
|
||||||
|
più
|
||||||
|
meno
|
||||||
|
bene
|
||||||
|
meglio
|
||||||
|
peggio
|
||||||
|
tanto
|
||||||
|
poco
|
||||||
|
troppo
|
||||||
|
abbastanza
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
# Spanish Common Words - Wordlist
|
||||||
|
# Palabras españolas comunes para frases de contraseña seguras
|
||||||
|
|
||||||
|
ser
|
||||||
|
haber
|
||||||
|
estar
|
||||||
|
tener
|
||||||
|
hacer
|
||||||
|
poder
|
||||||
|
decir
|
||||||
|
ir
|
||||||
|
ver
|
||||||
|
saber
|
||||||
|
querer
|
||||||
|
llegar
|
||||||
|
pasar
|
||||||
|
deber
|
||||||
|
poner
|
||||||
|
parecer
|
||||||
|
quedar
|
||||||
|
creer
|
||||||
|
hablar
|
||||||
|
llevar
|
||||||
|
tiempo
|
||||||
|
persona
|
||||||
|
año
|
||||||
|
vez
|
||||||
|
día
|
||||||
|
hombre
|
||||||
|
cosa
|
||||||
|
país
|
||||||
|
problema
|
||||||
|
mundo
|
||||||
|
vida
|
||||||
|
mano
|
||||||
|
familia
|
||||||
|
grupo
|
||||||
|
momento
|
||||||
|
pregunta
|
||||||
|
derecho
|
||||||
|
servicio
|
||||||
|
trabajo
|
||||||
|
lugar
|
||||||
|
hora
|
||||||
|
estado
|
||||||
|
gobierno
|
||||||
|
españa
|
||||||
|
español
|
||||||
|
nacional
|
||||||
|
centro
|
||||||
|
euro
|
||||||
|
política
|
||||||
|
ministro
|
||||||
|
presente
|
||||||
|
consejo
|
||||||
|
madrid
|
||||||
|
punto
|
||||||
|
curso
|
||||||
|
formación
|
||||||
|
información
|
||||||
|
sistema
|
||||||
|
equipo
|
||||||
|
proyecto
|
||||||
|
empresa
|
||||||
|
actividad
|
||||||
|
investigación
|
||||||
|
resultado
|
||||||
|
programa
|
||||||
|
sociedad
|
||||||
|
seguridad
|
||||||
|
comunidad
|
||||||
|
desarrollo
|
||||||
|
economía
|
||||||
|
mercado
|
||||||
|
relación
|
||||||
|
cultura
|
||||||
|
calidad
|
||||||
|
nivel
|
||||||
|
historia
|
||||||
|
región
|
||||||
|
territorio
|
||||||
|
administración
|
||||||
|
comunicación
|
||||||
|
dirección
|
||||||
|
misión
|
||||||
|
objetivo
|
||||||
|
organización
|
||||||
|
público
|
||||||
|
internacional
|
||||||
|
social
|
||||||
|
general
|
||||||
|
particular
|
||||||
|
personal
|
||||||
|
profesional
|
||||||
|
económico
|
||||||
|
europeo
|
||||||
|
grande
|
||||||
|
nuevo
|
||||||
|
primero
|
||||||
|
último
|
||||||
|
importante
|
||||||
|
posible
|
||||||
|
diferente
|
||||||
|
local
|
||||||
|
militar
|
||||||
|
cultural
|
||||||
|
natural
|
||||||
|
humano
|
||||||
|
civil
|
||||||
|
comercial
|
||||||
|
industrial
|
||||||
|
financiero
|
||||||
|
médico
|
||||||
|
científico
|
||||||
|
artístico
|
||||||
|
deportivo
|
||||||
|
educativo
|
||||||
|
alimentario
|
||||||
|
ambiental
|
||||||
|
bueno
|
||||||
|
malo
|
||||||
|
mucho
|
||||||
|
poco
|
||||||
|
todo
|
||||||
|
nada
|
||||||
|
siempre
|
||||||
|
nunca
|
||||||
|
hoy
|
||||||
|
ayer
|
||||||
|
mañana
|
||||||
|
ahora
|
||||||
|
antes
|
||||||
|
después
|
||||||
|
aquí
|
||||||
|
allí
|
||||||
|
donde
|
||||||
|
como
|
||||||
|
porque
|
||||||
|
cuando
|
||||||
|
cual
|
||||||
|
cuanto
|
||||||
|
quien
|
||||||
|
que
|
||||||
|
sobre
|
||||||
|
bajo
|
||||||
|
dentro
|
||||||
|
fuera
|
||||||
|
delante
|
||||||
|
detrás
|
||||||
|
junto
|
||||||
|
solo
|
||||||
|
también
|
||||||
|
todavía
|
||||||
|
más
|
||||||
|
menos
|
||||||
|
bien
|
||||||
|
mejor
|
||||||
|
peor
|
||||||
|
tanto
|
||||||
|
demasiado
|
||||||
|
bastante
|
||||||
|
agua
|
||||||
|
aire
|
||||||
|
tierra
|
||||||
|
fuego
|
||||||
|
sol
|
||||||
|
luna
|
||||||
|
estrella
|
||||||
|
mar
|
||||||
|
río
|
||||||
|
montaña
|
||||||
|
bosque
|
||||||
|
flor
|
||||||
|
árbol
|
||||||
|
casa
|
||||||
|
ciudad
|
||||||
|
pueblo
|
||||||
|
calle
|
||||||
|
camino
|
||||||
|
puerta
|
||||||
|
ventana
|
||||||
|
mesa
|
||||||
|
silla
|
||||||
|
cama
|
||||||
|
libro
|
||||||
|
papel
|
||||||
|
palabra
|
||||||
|
número
|
||||||
|
letra
|
||||||
|
color
|
||||||
|
blanco
|
||||||
|
negro
|
||||||
|
rojo
|
||||||
|
azul
|
||||||
|
verde
|
||||||
|
amarillo
|
||||||
|
grande
|
||||||
|
pequeño
|
||||||
|
alto
|
||||||
|
bajo
|
||||||
|
largo
|
||||||
|
corto
|
||||||
|
ancho
|
||||||
|
estrecho
|
||||||
|
gordo
|
||||||
|
delgado
|
||||||
|
joven
|
||||||
|
viejo
|
||||||
|
nuevo
|
||||||
|
antiguo
|
||||||
|
bueno
|
||||||
|
malo
|
||||||
|
bonito
|
||||||
|
feo
|
||||||
|
fácil
|
||||||
|
difícil
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,30 @@
|
|||||||
|
services:
|
||||||
|
password-generator:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: pw-generator:local
|
||||||
|
container_name: pw-generator
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
APP_ENV: production
|
||||||
|
APP_DEBUG: "false"
|
||||||
|
LOG_LEVEL: warning
|
||||||
|
PORT: "8080"
|
||||||
|
RATE_LIMIT: "120"
|
||||||
|
CORS_ORIGIN: "http://localhost:8080"
|
||||||
|
CORS_METHODS: "GET,POST,OPTIONS"
|
||||||
|
CORS_HEADERS: "Content-Type,Authorization"
|
||||||
|
volumes:
|
||||||
|
- ./logs:/var/www/html/logs
|
||||||
|
- ./config/config.json:/var/www/html/config/config.json:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/ >/dev/null || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 20s
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Passwort-Generator API - Vollständige Dokumentation</title>
|
||||||
|
<!-- Same styling -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="header">
|
||||||
|
<div class="container">
|
||||||
|
<h1>🔐 Passwort-Generator API - Vollständiger Leitfaden</h1>
|
||||||
|
<nav class="nav-links">
|
||||||
|
<a href="/">← Zurück zum Generator</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- High Entropy Generator in German -->
|
||||||
|
<div class="generator-section">
|
||||||
|
<div class="generator-header">
|
||||||
|
<h3>🔐 Hochentropie-Generator</h3>
|
||||||
|
<p>Kryptographisch sichere Zufallspasswörter mit vollständiger Zeichensatzkontrolle.</p>
|
||||||
|
</div>
|
||||||
|
<div class="generator-content">
|
||||||
|
<h4>Zweck & Anwendungsfall</h4>
|
||||||
|
<p>Generiert traditionelle Zufallspasswörter mit vollständiger Kontrolle über Zeichentypen. Am besten für Systeme, die spezifische Zeichensatzkonformität erfordern oder wenn Sie maximale Zufälligkeit benötigen.</p>
|
||||||
|
|
||||||
|
<h4>Verfügbare Optionen</h4>
|
||||||
|
|
||||||
|
<div class="option-detail">
|
||||||
|
<h4><span class="option-type">Zahl</span><span class="option-default">16</span>length (Länge)</h4>
|
||||||
|
<p><strong>Bereich:</strong> 8-128 Zeichen</p>
|
||||||
|
<p><strong>Beschreibung:</strong> Gesamte Passwortlänge. Längere Passwörter bieten exponentiell mehr Sicherheit.</p>
|
||||||
|
<p><strong>Sicherheitsauswirkung:</strong> Jedes zusätzliche Zeichen verdoppelt ungefähr die Zeit, die zum Knacken des Passworts erforderlich ist.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Continue with other options in German -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
+974
@@ -0,0 +1,974 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>API Documentation - Password Generator</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: white;
|
||||||
|
padding: 20px 0;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .nav-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .nav-links a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .nav-links a:hover {
|
||||||
|
background: #ecf0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-section {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-section h2 {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-section p {
|
||||||
|
color: #7f8c8d;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-url {
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
font-family: monospace;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloads-section {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloads-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-card {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-card h4 {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-card .icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-card p {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn {
|
||||||
|
display: inline-block;
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swagger-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swagger-header {
|
||||||
|
background: #34495e;
|
||||||
|
color: white;
|
||||||
|
padding: 20px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swagger-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swagger-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger-ui {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-get { background: #27ae60; color: white; }
|
||||||
|
.method-post { background: #e74c3c; color: white; }
|
||||||
|
.method-put { background: #f39c12; color: white; }
|
||||||
|
.method-delete { background: #e67e22; color: white; }
|
||||||
|
|
||||||
|
.features-section {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-item {
|
||||||
|
padding: 20px;
|
||||||
|
border-left: 4px solid #3498db;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 0 5px 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-item h4 {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-item p {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-example {
|
||||||
|
background: #2c3e50;
|
||||||
|
color: #ecf0f1;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn {
|
||||||
|
background: #34495e;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
float: right;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:hover {
|
||||||
|
background: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 60px;
|
||||||
|
padding: 40px 0;
|
||||||
|
background: white;
|
||||||
|
border-top: 1px solid #ecf0f1;
|
||||||
|
text-align: center;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: #27ae60;
|
||||||
|
color: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
z-index: 1000;
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.show {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Swagger UI Customizations */
|
||||||
|
.swagger-ui .info .title {
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swagger-ui .scheme-container {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swagger-ui .info .description {
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swagger-ui .opblock.opblock-get .opblock-summary-method {
|
||||||
|
background: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swagger-ui .opblock.opblock-post .opblock-summary-method {
|
||||||
|
background: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swagger-ui .opblock-tag {
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header .container {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloads-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="header">
|
||||||
|
<div class="container">
|
||||||
|
<h1>🔐 Password Generator API</h1>
|
||||||
|
<nav class="nav-links">
|
||||||
|
<a href="/">← Back to Generator</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main-container">
|
||||||
|
<div class="intro-section">
|
||||||
|
<h2>API Documentation</h2>
|
||||||
|
<p>Complete OpenAPI 3.0 specification for the Password Generator API. Generate secure passwords with multiple algorithms, manage wordlists dynamically, and support international languages.</p>
|
||||||
|
|
||||||
|
<div class="base-url">
|
||||||
|
Base URL: <code>http://localhost:8080/api/v1</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="features-grid">
|
||||||
|
<div class="feature-item">
|
||||||
|
<h4>🔐 Multiple Algorithms</h4>
|
||||||
|
<p>High-entropy, XKCD, Diceware, PWGen, and Passkey generation methods</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-item">
|
||||||
|
<h4>🌍 Internationalization</h4>
|
||||||
|
<p>Dynamic wordlist discovery and multi-language UI support</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-item">
|
||||||
|
<h4>📊 Entropy Analysis</h4>
|
||||||
|
<p>Real-time password strength calculation and security metrics</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-item">
|
||||||
|
<h4>🔧 Configurable</h4>
|
||||||
|
<p>Extensive customization options for all password types</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="downloads-section">
|
||||||
|
<h2>Download Configuration Files</h2>
|
||||||
|
<p>Get the complete API specification in various formats for integration with your tools and workflows.</p>
|
||||||
|
|
||||||
|
<div class="downloads-grid">
|
||||||
|
<div class="download-card">
|
||||||
|
<h4><span class="icon">📄</span> OpenAPI JSON</h4>
|
||||||
|
<p>Complete OpenAPI 3.0 specification in JSON format. Perfect for code generation, API testing, and integration with development tools.</p>
|
||||||
|
<a href="#" class="download-btn" id="download-json">Download JSON</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="download-card">
|
||||||
|
<h4><span class="icon">📋</span> OpenAPI YAML</h4>
|
||||||
|
<p>Human-readable YAML format specification. Ideal for documentation, version control, and collaborative API development.</p>
|
||||||
|
<a href="#" class="download-btn" id="download-yaml">Download YAML</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="download-card">
|
||||||
|
<h4><span class="icon">🚀</span> Postman Collection</h4>
|
||||||
|
<p>Ready-to-use Postman collection with all endpoints, examples, and environment variables for immediate API testing.</p>
|
||||||
|
<a href="#" class="download-btn" id="download-postman">Download Collection</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="download-card">
|
||||||
|
<h4><span class="icon">⚡</span> cURL Examples</h4>
|
||||||
|
<p>Complete set of cURL commands with examples for all endpoints. Perfect for command-line testing and scripting.</p>
|
||||||
|
<a href="#" class="download-btn" id="download-curl">Download Examples</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="swagger-container">
|
||||||
|
<div class="swagger-header">
|
||||||
|
<h3>Interactive API Explorer</h3>
|
||||||
|
</div>
|
||||||
|
<div class="swagger-content">
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="features-section">
|
||||||
|
<h2>Quick Start</h2>
|
||||||
|
<p>Get started with the Password Generator API in minutes. Here are some common examples:</p>
|
||||||
|
|
||||||
|
<h3>Generate XKCD Passphrase</h3>
|
||||||
|
<button class="copy-btn" onclick="copyToClipboard('curl-xkcd')">Copy</button>
|
||||||
|
<div class="code-example" id="curl-xkcd">curl -X POST "http://localhost:8080/api/v1/passwords/generate" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"type": "xkcdpass",
|
||||||
|
"count": 3,
|
||||||
|
"options": {
|
||||||
|
"words": 4,
|
||||||
|
"wordlist": "en",
|
||||||
|
"separator": "-",
|
||||||
|
"add_number": true,
|
||||||
|
"number_position": "random"
|
||||||
|
}
|
||||||
|
}'</div>
|
||||||
|
|
||||||
|
<h3>Check Available Wordlists</h3>
|
||||||
|
<button class="copy-btn" onclick="copyToClipboard('curl-wordlists')">Copy</button>
|
||||||
|
<div class="code-example" id="curl-wordlists">curl -X GET "http://localhost:8080/api/v1/passwords/wordlists"</div>
|
||||||
|
|
||||||
|
<h3>Generate High-Entropy Password</h3>
|
||||||
|
<button class="copy-btn" onclick="copyToClipboard('curl-entropy')">Copy</button>
|
||||||
|
<div class="code-example" id="curl-entropy">curl -X POST "http://localhost:8080/api/v1/passwords/generate" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"type": "high-entropy",
|
||||||
|
"count": 1,
|
||||||
|
"options": {
|
||||||
|
"length": 20,
|
||||||
|
"include_symbols": true
|
||||||
|
}
|
||||||
|
}'</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<p>
|
||||||
|
Password Generator API v2.0.0 •
|
||||||
|
<a href="/">Main Application</a> •
|
||||||
|
<a href="#swagger-ui">Interactive Docs</a>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js"></script>
|
||||||
|
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-standalone-preset.js"></script>
|
||||||
|
<script>
|
||||||
|
// OpenAPI Specification (embedded)
|
||||||
|
const spec = {
|
||||||
|
"openapi": "3.0.3",
|
||||||
|
"info": {
|
||||||
|
"title": "Password Generator API",
|
||||||
|
"description": "Secure password generation with multiple algorithms and customizable options. Generate high-entropy passwords, XKCD-style passphrases, Diceware passwords, and more with comprehensive internationalization support.",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"contact": {
|
||||||
|
"name": "Password Generator API",
|
||||||
|
"url": "http://localhost:8080"
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"name": "MIT",
|
||||||
|
"url": "https://opensource.org/licenses/MIT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8080/api/v1",
|
||||||
|
"description": "Development server"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"/passwords": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Get available password types",
|
||||||
|
"description": "Returns all available password generation types with their descriptions and configuration options.",
|
||||||
|
"operationId": "getPasswordTypes",
|
||||||
|
"tags": ["Password Generation"],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of available password generation types",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/PasswordTypesResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/passwords/generate": {
|
||||||
|
"post": {
|
||||||
|
"summary": "Generate passwords",
|
||||||
|
"description": "Generate one or more passwords using the specified type and custom options.",
|
||||||
|
"operationId": "generatePasswords",
|
||||||
|
"tags": ["Password Generation"],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/GeneratePasswordRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successfully generated passwords",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/GeneratePasswordResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid request parameters",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/passwords/wordlists": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Get available wordlists",
|
||||||
|
"description": "Returns all available wordlists for XKCD password generation with availability status. Used by frontend to dynamically populate language options.",
|
||||||
|
"operationId": "getAvailableWordlists",
|
||||||
|
"tags": ["Wordlists"],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of available wordlists",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/WordlistsResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/locales/{lang}": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Get localization strings",
|
||||||
|
"description": "Returns translation strings for the specified language code.",
|
||||||
|
"operationId": "getLocalization",
|
||||||
|
"tags": ["Internationalization"],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "lang",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "Language code (2-3 letters)",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z]{2,3}(_[A-Z]{2})?$",
|
||||||
|
"enum": ["en", "de", "de_CH", "fr", "it", "es", "pt", "ja", "zh"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Localization strings for the specified language",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/LocalizationResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/health": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Health check",
|
||||||
|
"description": "Returns the current health status of the API service.",
|
||||||
|
"operationId": "healthCheck",
|
||||||
|
"tags": ["System"],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "API is healthy",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HealthResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"PasswordTypesResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": { "type": "boolean" },
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"types": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/components/schemas/PasswordType"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"message": { "type": "string" },
|
||||||
|
"timestamp": { "type": "string", "format": "date-time" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PasswordType": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"description": { "type": "string" },
|
||||||
|
"options": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GeneratePasswordRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type"],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["high-entropy", "xkcdpass", "diceware", "pwgen", "passkey"]
|
||||||
|
},
|
||||||
|
"count": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 50,
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GeneratePasswordResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": { "type": "boolean" },
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"passwords": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"entropy_bits": { "type": "number" },
|
||||||
|
"generator_info": { "type": "string" },
|
||||||
|
"options_used": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"message": { "type": "string" },
|
||||||
|
"timestamp": { "type": "string", "format": "date-time" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"WordlistsResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": { "type": "boolean" },
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"wordlists": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/components/schemas/Wordlist"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"count": { "type": "integer" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"message": { "type": "string" },
|
||||||
|
"timestamp": { "type": "string", "format": "date-time" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Wordlist": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"description": { "type": "string" },
|
||||||
|
"file": { "type": "string" },
|
||||||
|
"available": { "type": "boolean" },
|
||||||
|
"fallback": { "type": "boolean" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LocalizationResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": { "type": "string" }
|
||||||
|
},
|
||||||
|
"HealthResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["healthy", "degraded", "unhealthy"]
|
||||||
|
},
|
||||||
|
"version": { "type": "string" },
|
||||||
|
"timestamp": { "type": "string", "format": "date-time" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ErrorResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": { "type": "boolean", "example": false },
|
||||||
|
"error": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": { "type": "string" },
|
||||||
|
"code": { "type": "string" },
|
||||||
|
"status": { "type": "integer" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestamp": { "type": "string", "format": "date-time" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
{ "name": "Password Generation", "description": "Core password generation endpoints" },
|
||||||
|
{ "name": "Wordlists", "description": "Wordlist management for XKCD passwords" },
|
||||||
|
{ "name": "Internationalization", "description": "Localization and language support" },
|
||||||
|
{ "name": "System", "description": "System health and monitoring" }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize Swagger UI
|
||||||
|
window.onload = function() {
|
||||||
|
SwaggerUIBundle({
|
||||||
|
url: '',
|
||||||
|
spec: spec,
|
||||||
|
dom_id: '#swagger-ui',
|
||||||
|
deepLinking: true,
|
||||||
|
presets: [
|
||||||
|
SwaggerUIBundle.presets.apis,
|
||||||
|
SwaggerUIStandalonePreset
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
SwaggerUIBundle.plugins.DownloadUrl
|
||||||
|
],
|
||||||
|
layout: "StandaloneLayout",
|
||||||
|
tryItOutEnabled: true,
|
||||||
|
requestInterceptor: (request) => {
|
||||||
|
// Add any custom headers or modifications
|
||||||
|
return request;
|
||||||
|
},
|
||||||
|
responseInterceptor: (response) => {
|
||||||
|
// Handle responses
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Download functions
|
||||||
|
function downloadFile(content, filename, contentType) {
|
||||||
|
const blob = new Blob([content], { type: contentType });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
showNotification(`Downloaded ${filename}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotification(message) {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'notification';
|
||||||
|
notification.textContent = message;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => notification.classList.add('show'), 10);
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('show');
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download event handlers
|
||||||
|
document.getElementById('download-json').addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
downloadFile(JSON.stringify(spec, null, 2), 'password-generator-api.json', 'application/json');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('download-yaml').addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
// Convert JSON to YAML (simplified)
|
||||||
|
const yaml = jsonToYaml(spec);
|
||||||
|
downloadFile(yaml, 'password-generator-api.yaml', 'text/yaml');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('download-postman').addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const postmanCollection = generatePostmanCollection(spec);
|
||||||
|
downloadFile(JSON.stringify(postmanCollection, null, 2), 'password-generator-api.postman_collection.json', 'application/json');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('download-curl').addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const curlExamples = generateCurlExamples(spec);
|
||||||
|
downloadFile(curlExamples, 'password-generator-api-examples.sh', 'text/plain');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy to clipboard function
|
||||||
|
function copyToClipboard(elementId) {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
const text = element.textContent;
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
showNotification('Copied to clipboard!');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility functions for file generation
|
||||||
|
function jsonToYaml(obj, indent = 0) {
|
||||||
|
let yaml = '';
|
||||||
|
const spaces = ' '.repeat(indent);
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
if (value === null) {
|
||||||
|
yaml += `${spaces}${key}: null\n`;
|
||||||
|
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
yaml += `${spaces}${key}:\n`;
|
||||||
|
yaml += jsonToYaml(value, indent + 1);
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
yaml += `${spaces}${key}:\n`;
|
||||||
|
value.forEach(item => {
|
||||||
|
if (typeof item === 'object') {
|
||||||
|
yaml += `${spaces} -\n`;
|
||||||
|
yaml += jsonToYaml(item, indent + 2);
|
||||||
|
} else {
|
||||||
|
yaml += `${spaces} - ${item}\n`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
yaml += `${spaces}${key}: ${typeof value === 'string' ? `"${value}"` : value}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return yaml;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePostmanCollection(spec) {
|
||||||
|
return {
|
||||||
|
info: {
|
||||||
|
name: spec.info.title,
|
||||||
|
description: spec.info.description,
|
||||||
|
version: spec.info.version,
|
||||||
|
schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
variable: [
|
||||||
|
{
|
||||||
|
key: "baseUrl",
|
||||||
|
value: spec.servers[0].url,
|
||||||
|
type: "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
item: Object.entries(spec.paths).map(([path, methods]) => {
|
||||||
|
return Object.entries(methods).map(([method, operation]) => ({
|
||||||
|
name: operation.summary,
|
||||||
|
request: {
|
||||||
|
method: method.toUpperCase(),
|
||||||
|
header: method === 'post' ? [
|
||||||
|
{
|
||||||
|
key: "Content-Type",
|
||||||
|
value: "application/json"
|
||||||
|
}
|
||||||
|
] : [],
|
||||||
|
url: {
|
||||||
|
raw: `{{baseUrl}}${path}`,
|
||||||
|
host: ["{{baseUrl}}"],
|
||||||
|
path: path.split('/').filter(p => p)
|
||||||
|
},
|
||||||
|
body: method === 'post' ? {
|
||||||
|
mode: "raw",
|
||||||
|
raw: JSON.stringify({
|
||||||
|
type: "xkcdpass",
|
||||||
|
count: 1,
|
||||||
|
options: {
|
||||||
|
words: 4,
|
||||||
|
language: "en"
|
||||||
|
}
|
||||||
|
}, null, 2)
|
||||||
|
} : undefined
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}).flat()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateCurlExamples(spec) {
|
||||||
|
const baseUrl = spec.servers[0].url;
|
||||||
|
let examples = `#!/bin/bash
|
||||||
|
# Password Generator API - cURL Examples
|
||||||
|
# Base URL: ${baseUrl}
|
||||||
|
|
||||||
|
echo "Password Generator API Examples"
|
||||||
|
echo "=============================="
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
Object.entries(spec.paths).forEach(([path, methods]) => {
|
||||||
|
Object.entries(methods).forEach(([method, operation]) => {
|
||||||
|
examples += `# ${operation.summary}\n`;
|
||||||
|
examples += `echo "\\n${operation.summary}:"\n`;
|
||||||
|
|
||||||
|
if (method === 'get') {
|
||||||
|
examples += `curl -X GET "${baseUrl}${path}"\n\n`;
|
||||||
|
} else if (method === 'post') {
|
||||||
|
examples += `curl -X POST "${baseUrl}${path}" \\\n`;
|
||||||
|
examples += ` -H "Content-Type: application/json" \\\n`;
|
||||||
|
examples += ` -d '{\n`;
|
||||||
|
examples += ` "type": "xkcdpass",\n`;
|
||||||
|
examples += ` "count": 1,\n`;
|
||||||
|
examples += ` "options": {\n`;
|
||||||
|
examples += ` "words": 4,\n`;
|
||||||
|
examples += ` "wordlist": "en",\n`;
|
||||||
|
examples += ` "add_number": true\n`;
|
||||||
|
examples += ` }\n`;
|
||||||
|
examples += ` }'\n\n`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return examples;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+268
@@ -0,0 +1,268 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Password Generator API - Complete Documentation</title>
|
||||||
|
<link rel="stylesheet" href="../assets/css/style.css" />
|
||||||
|
<link rel="stylesheet" href="../assets/css/style-docs.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="language-selector">
|
||||||
|
<select id="language-select" class="language-select form-control form-control--language" onchange="switchLanguage(this.value)">
|
||||||
|
<option value="en">🇺🇸 English</option>
|
||||||
|
<option value="de">🇩🇪 Deutsch</option>
|
||||||
|
<option value="de_CH">🇨🇭 Schwyzerdütsch</option>
|
||||||
|
<option value="fr">🇫🇷 Français</option>
|
||||||
|
<option value="es">🇪🇸 Español</option>
|
||||||
|
<option value="pt">🇵🇹 Portugues</option>
|
||||||
|
<option value="it">🇮🇹 Italiano</option>
|
||||||
|
<option value="ja">🇯🇵 日本語</option>
|
||||||
|
<option value="zh">🇨🇳 中文</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<header class="header">
|
||||||
|
<div class="container">
|
||||||
|
<h1>🔐 Password Generator API - Complete Guide</h1>
|
||||||
|
<nav class="nav-links">
|
||||||
|
<a href="/">← Back to Generator</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main-container">
|
||||||
|
<!-- Base URL and Quick Start -->
|
||||||
|
<div class="intro-section">
|
||||||
|
<h2>API Overview</h2>
|
||||||
|
<div class="base-url">
|
||||||
|
Base URL: <code>http://localhost:8080/api/v1</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="security-note">
|
||||||
|
All password generation uses cryptographically secure random number generation for maximum security.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- High Entropy Generator -->
|
||||||
|
<div class="generator-section">
|
||||||
|
<div class="generator-header">
|
||||||
|
<h3>🔐 High Entropy Generator</h3>
|
||||||
|
<p>Cryptographically secure random passwords with full character set control.</p>
|
||||||
|
</div>
|
||||||
|
<div class="generator-content">
|
||||||
|
<h4>Purpose & Use Case</h4>
|
||||||
|
<p>Generate traditional random passwords with complete control over character types. Best for systems requiring specific character set compliance or when you need maximum randomness.</p>
|
||||||
|
|
||||||
|
<h4>Available Options</h4>
|
||||||
|
|
||||||
|
<div class="option-detail">
|
||||||
|
<h4><span class="option-type">number</span><span class="option-default">16</span>length</h4>
|
||||||
|
<p><strong>Range:</strong> 8-128 characters</p>
|
||||||
|
<p><strong>Description:</strong> Total password length. Longer passwords provide exponentially more security.</p>
|
||||||
|
<p><strong>Security Impact:</strong> Each additional character roughly doubles the time required to crack the password.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-detail">
|
||||||
|
<h4><span class="option-type">boolean</span><span class="option-default">true</span>include_uppercase</h4>
|
||||||
|
<p><strong>Description:</strong> Include uppercase letters (A-Z) in the character set.</p>
|
||||||
|
<p><strong>Character Set:</strong> ABCDEFGHIJKLMNOPQRSTUVWXYZ (26 characters)</p>
|
||||||
|
<p><strong>Security Impact:</strong> Adds 26 possible characters, significantly increasing entropy.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-detail">
|
||||||
|
<h4><span class="option-type">boolean</span><span class="option-default">true</span>include_lowercase</h4>
|
||||||
|
<p><strong>Description:</strong> Include lowercase letters (a-z) in the character set.</p>
|
||||||
|
<p><strong>Character Set:</strong> abcdefghijklmnopqrstuvwxyz (26 characters)</p>
|
||||||
|
<p><strong>Note:</strong> At least one character type must be selected.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-detail">
|
||||||
|
<h4><span class="option-type">boolean</span><span class="option-default">true</span>include_numbers</h4>
|
||||||
|
<p><strong>Description:</strong> Include numeric digits (0-9) in the character set.</p>
|
||||||
|
<p><strong>Character Set:</strong> 0123456789 (10 characters)</p>
|
||||||
|
<p><strong>Compliance:</strong> Required by many corporate password policies.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-detail">
|
||||||
|
<h4><span class="option-type">boolean</span><span class="option-default">true</span>include_symbols</h4>
|
||||||
|
<p><strong>Description:</strong> Include special symbols in the character set.</p>
|
||||||
|
<p><strong>Character Set:</strong> !@#$%^&*()_+-=[]{}|;:,.<>? (25 characters)</p>
|
||||||
|
<p><strong>Note:</strong> Some systems have restrictions on certain symbols. Validate accepted characters in your target environment.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4>Example Request</h4>
|
||||||
|
<div class="example-request">
|
||||||
|
POST /api/v1/passwords/generate
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "high-entropy",
|
||||||
|
"count": 2,
|
||||||
|
"options": {
|
||||||
|
"length": 20,
|
||||||
|
"include_uppercase": true,
|
||||||
|
"include_lowercase": true,
|
||||||
|
"include_numbers": true,
|
||||||
|
"include_symbols": false
|
||||||
|
}
|
||||||
|
}</div>
|
||||||
|
|
||||||
|
<h4>Example Response</h4>
|
||||||
|
<div class="example-response">
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"passwords": [
|
||||||
|
"Km9xB3nQ2pL8vR5wE6sA",
|
||||||
|
"T7gY4hJ9kM1nP6qU8zC3"
|
||||||
|
],
|
||||||
|
"entropy_bits": 119.5,
|
||||||
|
"generator_info": "High entropy generator with alphanumeric characters",
|
||||||
|
"options_used": {
|
||||||
|
"length": 20,
|
||||||
|
"include_uppercase": true,
|
||||||
|
"include_lowercase": true,
|
||||||
|
"include_numbers": true,
|
||||||
|
"include_symbols": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"message": "Generated 2 password(s) of type 'high-entropy'",
|
||||||
|
"timestamp": "2025-08-23T21:24:00+02:00"
|
||||||
|
}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- XKCD Generator -->
|
||||||
|
<div class="generator-section">
|
||||||
|
<div class="generator-header">
|
||||||
|
<h3>🗣️ XKCD Pass Generator</h3>
|
||||||
|
<p>Memorable passphrases using word combinations with multilingual support.</p>
|
||||||
|
</div>
|
||||||
|
<div class="generator-content">
|
||||||
|
<h4>Purpose & Use Case</h4>
|
||||||
|
<p>Generate memorable passphrases based on the famous XKCD comic "Password Strength". Ideal when users need to remember passwords without writing them down. Supports multiple languages for international users.</p>
|
||||||
|
|
||||||
|
<h4>Available Options</h4>
|
||||||
|
|
||||||
|
<div class="option-detail">
|
||||||
|
<h4><span class="option-type">number</span><span class="option-default">4</span>words</h4>
|
||||||
|
<p><strong>Range:</strong> 3-8 words</p>
|
||||||
|
<p><strong>Description:</strong> Number of words in the passphrase.</p>
|
||||||
|
<p><strong>Security Impact:</strong> Each additional word exponentially increases security. 4 words ≈ 52 bits, 6 words ≈ 77 bits entropy.</p>
|
||||||
|
<p><strong>Memorability:</strong> 4-5 words strike the best balance between security and memorability.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-detail">
|
||||||
|
<h4><span class="option-type">select</span><span class="option-default">en</span>wordlist</h4>
|
||||||
|
<p><strong>Options:</strong> en, de, de_CH, fr, it, es, pt, ja, zh</p>
|
||||||
|
<p><strong>Description:</strong> Wordlist to use for generation.</p>
|
||||||
|
<p><strong>Wordlist Quality:</strong> Built-in curated collections are selected per language and can be checked dynamically from the wordlist endpoint.</p>
|
||||||
|
<p><strong>Availability:</strong> Check <code>/api/v1/passwords/wordlists</code> for current language availability.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-detail">
|
||||||
|
<h4><span class="option-type">text</span><span class="option-default">-</span>separator</h4>
|
||||||
|
<p><strong>Description:</strong> Character(s) used to separate words in the passphrase.</p>
|
||||||
|
<p><strong>Single Character:</strong> All separations use the same character (e.g., "word-word-word").</p>
|
||||||
|
<p><strong>Multiple Characters:</strong> Each separation randomly selects from the character set (e.g., "word-word_word.word").</p>
|
||||||
|
<p><strong>Space Handling:</strong> Spaces are excluded from multi-character separator sets for security.</p>
|
||||||
|
<p><strong>Examples:</strong> "-", "_", ".", "-_.", "!@#"</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-detail">
|
||||||
|
<h4><span class="option-type">boolean</span><span class="option-default">false</span>add_number</h4>
|
||||||
|
<p><strong>Description:</strong> Add a 2-digit number to the passphrase.</p>
|
||||||
|
<p><strong>Security Benefit:</strong> Increases entropy by ~6.6 bits (100 possible values).</p>
|
||||||
|
<p><strong>Position Control:</strong> Use <code>number_position</code> to control placement.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-detail">
|
||||||
|
<h4><span class="option-type">select</span><span class="option-default">end</span>number_position</h4>
|
||||||
|
<p><strong>Requires:</strong> <code>add_number: true</code></p>
|
||||||
|
<p><strong>Options:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>beginning</code>: "42-word-word-word"</li>
|
||||||
|
<li><code>end</code>: "word-word-word-42"</li>
|
||||||
|
<li><code>random</code>: "word-42-word-word" (random separator position)</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Security Note:</strong> Random position provides additional unpredictability.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4>Example Request</h4>
|
||||||
|
<div class="example-request">
|
||||||
|
POST /api/v1/passwords/generate
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "xkcdpass",
|
||||||
|
"count": 1,
|
||||||
|
"options": {
|
||||||
|
"words": 5,
|
||||||
|
"wordlist": "en",
|
||||||
|
"separator": "-_.",
|
||||||
|
"add_number": true,
|
||||||
|
"number_position": "random"
|
||||||
|
}
|
||||||
|
}</div>
|
||||||
|
|
||||||
|
<h4>Example Response</h4>
|
||||||
|
<div class="example-response">
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"passwords": ["horse-battery_42.correct_staple"],
|
||||||
|
"explanations": ["large four-legged animal – device that stores electrical energy – free from error; accurate – basic or principal item"],
|
||||||
|
"entropy_bits": 58.7,
|
||||||
|
"generator_info": "XKCD-style passphrase using 5 words from en wordlist",
|
||||||
|
"options_used": {
|
||||||
|
"words": 5,
|
||||||
|
"wordlist": "en",
|
||||||
|
"separator": "-_.",
|
||||||
|
"add_number": true,
|
||||||
|
"number_position": "random"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"message": "Generated 1 password(s) of type 'xkcdpass'",
|
||||||
|
"timestamp": "2025-08-23T21:24:00+02:00"
|
||||||
|
}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add similar detailed sections for Diceware, PWGen, and Passkey generators -->
|
||||||
|
|
||||||
|
<!-- Security Recommendations -->
|
||||||
|
<div class="generator-section">
|
||||||
|
<div class="generator-header">
|
||||||
|
<h3>🔒 Security Recommendations</h3>
|
||||||
|
</div>
|
||||||
|
<div class="generator-content">
|
||||||
|
<h4>Choosing the Right Generator</h4>
|
||||||
|
<ul>
|
||||||
|
<li><strong>High Entropy:</strong> Maximum security with strict policy control</li>
|
||||||
|
<li><strong>XKCD Pass:</strong> User memorability, multi-language support needed</li>
|
||||||
|
<li><strong>Diceware:</strong> Maximum security with educational value</li>
|
||||||
|
<li><strong>PWGen:</strong> Pronounceable passwords with balanced strength and usability</li>
|
||||||
|
<li><strong>Passkey:</strong> Pronounceable passwords for verbal communication</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>Entropy Guidelines</h4>
|
||||||
|
<ul>
|
||||||
|
<li><strong>50+ bits:</strong> Basic security (personal accounts)</li>
|
||||||
|
<li><strong>65+ bits:</strong> Strong security (business accounts)</li>
|
||||||
|
<li><strong>80+ bits:</strong> Excellent security (high-value accounts)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function switchLanguage(lang) {
|
||||||
|
// Redirect to language-specific documentation
|
||||||
|
const baseUrl = window.location.origin + window.location.pathname;
|
||||||
|
const newUrl = baseUrl.replace('/docs', `/docs-${lang}`);
|
||||||
|
window.location.href = newUrl;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Password Generator Installation Script
|
||||||
|
# This script sets up the password generator with i18n support
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔐 Password Generator Installation"
|
||||||
|
echo "=================================="
|
||||||
|
|
||||||
|
# Check PHP version
|
||||||
|
php_version=$(php -v | head -n1 | cut -d' ' -f2 | cut -d'.' -f1,2)
|
||||||
|
required_version="8.1"
|
||||||
|
|
||||||
|
if [ "$(printf '%s\n' "$required_version" "$php_version" | sort -V | head -n1)" != "$required_version" ]; then
|
||||||
|
echo "❌ PHP 8.1+ required (found: $php_version)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ PHP version: $php_version"
|
||||||
|
|
||||||
|
# Check if composer is installed
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
echo "❌ Composer not found. Please install Composer first:"
|
||||||
|
echo " https://getcomposer.org/download/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Composer found"
|
||||||
|
|
||||||
|
# Install PHP dependencies
|
||||||
|
echo ""
|
||||||
|
echo "📦 Installing PHP dependencies..."
|
||||||
|
composer install --no-dev --optimize-autoloader
|
||||||
|
|
||||||
|
# Set up environment file
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo ""
|
||||||
|
echo "⚙️ Setting up environment configuration..."
|
||||||
|
cp .env.example .env
|
||||||
|
echo "✅ Environment file created (.env)"
|
||||||
|
echo " You can edit this file to customize settings"
|
||||||
|
else
|
||||||
|
echo "✅ Environment file already exists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create logs directory and set permissions
|
||||||
|
echo ""
|
||||||
|
echo "📁 Setting up directories..."
|
||||||
|
mkdir -p logs
|
||||||
|
chmod 755 logs
|
||||||
|
echo "✅ Logs directory created"
|
||||||
|
|
||||||
|
# Check if pwgen is available (optional)
|
||||||
|
if command -v pwgen &> /dev/null; then
|
||||||
|
echo "✅ pwgen found (enhanced compatibility enabled)"
|
||||||
|
else
|
||||||
|
echo "ℹ️ pwgen not found (will use PHP fallback)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test the installation
|
||||||
|
echo ""
|
||||||
|
echo "🧪 Testing installation..."
|
||||||
|
php -l public/index.php > /dev/null
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "✅ PHP syntax check passed"
|
||||||
|
else
|
||||||
|
echo "❌ PHP syntax error found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start the development server
|
||||||
|
echo ""
|
||||||
|
echo "🚀 Installation completed successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "To start the development server:"
|
||||||
|
echo " php -S localhost:8080 -t public"
|
||||||
|
echo ""
|
||||||
|
echo "To start with composer:"
|
||||||
|
echo " composer start"
|
||||||
|
echo ""
|
||||||
|
echo "Then visit: http://localhost:8080"
|
||||||
|
echo ""
|
||||||
|
echo "📚 Features:"
|
||||||
|
echo " • REST API for password generation"
|
||||||
|
echo " • Multilingual web interface (8 languages)"
|
||||||
|
echo " • 5 different password generation algorithms"
|
||||||
|
echo " • Security analysis and entropy calculation"
|
||||||
|
echo " • Password manager integration examples"
|
||||||
|
echo ""
|
||||||
|
echo "📖 Documentation: http://localhost:8080/docs"
|
||||||
|
echo "🌍 Supported languages: EN, DE, FR, IT, ES, PT, JA, ZH"
|
||||||
@@ -0,0 +1,545 @@
|
|||||||
|
# Password Generator API Integration Guide
|
||||||
|
|
||||||
|
This guide provides comprehensive examples for integrating the Password Generator API with popular password managers, launchers, and automation tools.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
- [Quick Start](#quick-start)
|
||||||
|
- [Password Managers](#password-managers)
|
||||||
|
- [Launchers & Tools](#launchers--tools)
|
||||||
|
- [Automation Scripts](#automation-scripts)
|
||||||
|
- [Security Considerations](#security-considerations)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
The API provides several endpoints for password generation:
|
||||||
|
|
||||||
|
**Base URL:** `http://localhost:8080/api/v1`
|
||||||
|
|
||||||
|
**Generate Password:**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/v1/passwords/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"type":"diceware","count":1,"options":{"words":6}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Password Managers
|
||||||
|
|
||||||
|
### Bitwarden
|
||||||
|
|
||||||
|
#### Bitwarden CLI Integration
|
||||||
|
|
||||||
|
**Installation:**
|
||||||
|
```bash
|
||||||
|
npm install -g @bitwarden/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
**Basic Password Generation and Storage:**
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# generate-and-store-bitwarden.sh
|
||||||
|
|
||||||
|
# Login to Bitwarden (do this once)
|
||||||
|
bw login
|
||||||
|
|
||||||
|
# Unlock vault (enter master password when prompted)
|
||||||
|
export BW_SESSION="$(bw unlock --raw)"
|
||||||
|
|
||||||
|
# Generate high-entropy password
|
||||||
|
PASSWORD=$(curl -s -X POST http://localhost:8080/api/v1/passwords/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"type":"high-entropy","count":1,"options":{"length":20,"includeSymbols":true}}' | \
|
||||||
|
jq -r '.data.passwords[0]')
|
||||||
|
|
||||||
|
# Create new login item
|
||||||
|
ITEM_DATA=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"name": "Generated Password $(date +%Y-%m-%d)",
|
||||||
|
"login": {
|
||||||
|
"username": "user@example.com",
|
||||||
|
"password": "$PASSWORD"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "$ITEM_DATA" | bw encode | bw create item
|
||||||
|
|
||||||
|
echo "Password created and stored in Bitwarden: $PASSWORD"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Advanced: Generate Different Password Types:**
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# bitwarden-multi-generator.sh
|
||||||
|
|
||||||
|
generate_and_store() {
|
||||||
|
local type=$1
|
||||||
|
local name=$2
|
||||||
|
local options=$3
|
||||||
|
|
||||||
|
PASSWORD=$(curl -s -X POST http://localhost:8080/api/v1/passwords/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"type\":\"$type\",\"count\":1,\"options\":$options}" | \
|
||||||
|
jq -r '.data.passwords[0]')
|
||||||
|
|
||||||
|
ITEM_DATA="{\"type\": 1, \"name\": \"$name\", \"login\": {\"password\": \"$PASSWORD\"}}"
|
||||||
|
echo "$ITEM_DATA" | bw encode | bw create item
|
||||||
|
|
||||||
|
echo "Created $name: $PASSWORD"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate different types
|
||||||
|
generate_and_store "diceware" "Diceware Passphrase" '{"words":6,"capitalize":true}'
|
||||||
|
generate_and_store "passkey" "Apple-style Password" '{"syllables":4,"addNumbers":true}'
|
||||||
|
generate_and_store "xkcdpass" "XKCD Passphrase" '{"words":4,"language":"english","addNumber":true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1Password
|
||||||
|
|
||||||
|
#### 1Password CLI Integration
|
||||||
|
|
||||||
|
**Installation:**
|
||||||
|
Follow 1Password CLI installation guide for your platform.
|
||||||
|
|
||||||
|
**Basic Integration:**
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# generate-and-store-1password.sh
|
||||||
|
|
||||||
|
# Generate secure password
|
||||||
|
PASSWORD=$(curl -s -X POST http://localhost:8080/api/v1/passwords/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"type":"high-entropy","count":1,"options":{"length":24}}' | \
|
||||||
|
jq -r '.data.passwords[0]')
|
||||||
|
|
||||||
|
# Create login item
|
||||||
|
op item create \
|
||||||
|
--category=login \
|
||||||
|
--title="Generated Password $(date +%Y-%m-%d)" \
|
||||||
|
--field="username=user@example.com" \
|
||||||
|
--field="password=$PASSWORD"
|
||||||
|
|
||||||
|
echo "Password stored in 1Password: $PASSWORD"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Batch Password Generation:**
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# 1password-batch-generator.sh
|
||||||
|
|
||||||
|
TYPES=("diceware" "high-entropy" "passkey" "xkcdpass")
|
||||||
|
NAMES=("Diceware Phrase" "High Entropy" "Passkey Style" "XKCD Phrase")
|
||||||
|
|
||||||
|
for i in "${!TYPES[@]}"; do
|
||||||
|
TYPE=${TYPES[$i]}
|
||||||
|
NAME=${NAMES[$i]}
|
||||||
|
|
||||||
|
PASSWORD=$(curl -s -X POST http://localhost:8080/api/v1/passwords/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"type\":\"$TYPE\",\"count\":1}" | \
|
||||||
|
jq -r '.data.passwords[0]')
|
||||||
|
|
||||||
|
op item create \
|
||||||
|
--category=login \
|
||||||
|
--title="$NAME $(date +%Y-%m-%d)" \
|
||||||
|
--field="password=$PASSWORD"
|
||||||
|
|
||||||
|
echo "Created $NAME: $PASSWORD"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### KeePass
|
||||||
|
|
||||||
|
#### KeePassXC CLI Integration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# keepassxc-generator.sh
|
||||||
|
|
||||||
|
DB_PATH="$HOME/passwords.kdbx"
|
||||||
|
KEY_FILE="$HOME/keyfile.key"
|
||||||
|
|
||||||
|
# Generate password
|
||||||
|
PASSWORD=$(curl -s -X POST http://localhost:8080/api/v1/passwords/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"type":"diceware","count":1,"options":{"words":6}}' | \
|
||||||
|
jq -r '.data.passwords[0]')
|
||||||
|
|
||||||
|
# Add to KeePass database
|
||||||
|
keepassxc-cli add "$DB_PATH" \
|
||||||
|
--key-file "$KEY_FILE" \
|
||||||
|
"Generated/$(date +%Y-%m-%d)" \
|
||||||
|
--username "user@example.com" \
|
||||||
|
--password-prompt
|
||||||
|
|
||||||
|
echo "Password: $PASSWORD"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Launchers & Tools
|
||||||
|
|
||||||
|
### Raycast
|
||||||
|
|
||||||
|
#### Simple Password Generator Extension
|
||||||
|
|
||||||
|
Create a Raycast script command:
|
||||||
|
|
||||||
|
**generate-password.sh:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Required parameters:
|
||||||
|
# @raycast.schemaVersion 1
|
||||||
|
# @raycast.title Generate Secure Password
|
||||||
|
# @raycast.mode compact
|
||||||
|
# @raycast.icon 🔐
|
||||||
|
# @raycast.packageName Password Generator
|
||||||
|
# @raycast.argument1 {"type": "dropdown", "placeholder": "Password Type", "data": [{"title": "High Entropy", "value": "high-entropy"}, {"title": "Diceware", "value": "diceware"}, {"title": "Passkey", "value": "passkey"}, {"title": "XKCD", "value": "xkcdpass"}]}
|
||||||
|
|
||||||
|
TYPE=${1:-"passkey"}
|
||||||
|
|
||||||
|
case $TYPE in
|
||||||
|
"high-entropy")
|
||||||
|
OPTIONS='{"length":16,"includeSymbols":true}'
|
||||||
|
;;
|
||||||
|
"diceware")
|
||||||
|
OPTIONS='{"words":5,"capitalize":true}'
|
||||||
|
;;
|
||||||
|
"passkey")
|
||||||
|
OPTIONS='{"syllables":3,"addNumbers":true}'
|
||||||
|
;;
|
||||||
|
"xkcdpass")
|
||||||
|
OPTIONS='{"words":4,"capitalize":true,"addNumber":true}'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
PASSWORD=$(curl -s -X POST http://localhost:8080/api/v1/passwords/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"type\":\"$TYPE\",\"count\":1,\"options\":$OPTIONS}" | \
|
||||||
|
jq -r '.data.passwords[0]')
|
||||||
|
|
||||||
|
if [ -n "$PASSWORD" ]; then
|
||||||
|
echo "$PASSWORD" | pbcopy
|
||||||
|
echo "✅ Password copied: $PASSWORD"
|
||||||
|
else
|
||||||
|
echo "❌ Failed to generate password"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Advanced Raycast Extension with Options
|
||||||
|
|
||||||
|
**raycast-advanced.sh:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Required parameters:
|
||||||
|
# @raycast.schemaVersion 1
|
||||||
|
# @raycast.title Advanced Password Generator
|
||||||
|
# @raycast.mode fullOutput
|
||||||
|
# @raycast.icon 🔐
|
||||||
|
# @raycast.packageName Password Generator
|
||||||
|
# @raycast.argument1 {"type": "text", "placeholder": "Number of passwords", "optional": true}
|
||||||
|
|
||||||
|
COUNT=${1:-1}
|
||||||
|
|
||||||
|
# Generate multiple password types
|
||||||
|
echo "🔐 Password Generator Results"
|
||||||
|
echo "================================"
|
||||||
|
|
||||||
|
# High Entropy
|
||||||
|
PASSWORD=$(curl -s -X POST http://localhost:8080/api/v1/passwords/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"type\":\"high-entropy\",\"count\":$COUNT,\"options\":{\"length\":16}}" | \
|
||||||
|
jq -r '.data.passwords[]' | head -1)
|
||||||
|
echo "🔒 High Entropy: $PASSWORD"
|
||||||
|
|
||||||
|
# Diceware
|
||||||
|
PASSWORD=$(curl -s -X POST http://localhost:8080/api/v1/passwords/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"type\":\"diceware\",\"count\":$COUNT,\"options\":{\"words\":5}}" | \
|
||||||
|
jq -r '.data.passwords[]' | head -1)
|
||||||
|
echo "🎲 Diceware: $PASSWORD"
|
||||||
|
|
||||||
|
# Passkey
|
||||||
|
PASSWORD=$(curl -s -X POST http://localhost:8080/api/v1/passwords/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"type\":\"passkey\",\"count\":$COUNT,\"options\":{\"syllables\":3}}" | \
|
||||||
|
jq -r '.data.passwords[]' | head -1)
|
||||||
|
echo "🗝️ Passkey: $PASSWORD"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Select a password above to copy it."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alfred
|
||||||
|
|
||||||
|
#### Alfred Workflow Script Filter
|
||||||
|
|
||||||
|
**alfred-password-generator.php:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$types = [
|
||||||
|
'high-entropy' => ['name' => 'High Entropy', 'icon' => '🔒', 'options' => '{"length":16}'],
|
||||||
|
'diceware' => ['name' => 'Diceware', 'icon' => '🎲', 'options' => '{"words":5}'],
|
||||||
|
'passkey' => ['name' => 'Passkey', 'icon' => '🗝️', 'options' => '{"syllables":3}'],
|
||||||
|
'xkcdpass' => ['name' => 'XKCD', 'icon' => '💭', 'options' => '{"words":4}']
|
||||||
|
];
|
||||||
|
|
||||||
|
$items = [];
|
||||||
|
|
||||||
|
foreach ($types as $type => $config) {
|
||||||
|
$items[] = [
|
||||||
|
'uid' => $type,
|
||||||
|
'title' => $config['name'] . ' Password',
|
||||||
|
'subtitle' => 'Generate ' . strtolower($config['name']) . ' password',
|
||||||
|
'arg' => $type,
|
||||||
|
'icon' => ['emoji' => $config['icon']]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['items' => $items]);
|
||||||
|
?>
|
||||||
|
```
|
||||||
|
|
||||||
|
**alfred-generate.sh:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
TYPE=$1
|
||||||
|
|
||||||
|
case $TYPE in
|
||||||
|
"high-entropy")
|
||||||
|
OPTIONS='{"length":16,"includeSymbols":true}'
|
||||||
|
;;
|
||||||
|
"diceware")
|
||||||
|
OPTIONS='{"words":5,"capitalize":true}'
|
||||||
|
;;
|
||||||
|
"passkey")
|
||||||
|
OPTIONS='{"syllables":3,"addNumbers":true}'
|
||||||
|
;;
|
||||||
|
"xkcdpass")
|
||||||
|
OPTIONS='{"words":4,"capitalize":true,"addNumber":true}'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
PASSWORD=$(curl -s -X POST http://localhost:8080/api/v1/passwords/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"type\":\"$TYPE\",\"count\":1,\"options\":$OPTIONS}" | \
|
||||||
|
jq -r '.data.passwords[0]')
|
||||||
|
|
||||||
|
echo "$PASSWORD" | pbcopy
|
||||||
|
echo "$PASSWORD"
|
||||||
|
```
|
||||||
|
|
||||||
|
### LaunchBar
|
||||||
|
|
||||||
|
#### LaunchBar Action
|
||||||
|
|
||||||
|
**PasswordGenerator.lbaction/Contents/Scripts/default.sh:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
PASSWORD=$(curl -s -X POST http://localhost:8080/api/v1/passwords/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"type":"passkey","count":1,"options":{"syllables":4,"addNumbers":true}}' | \
|
||||||
|
jq -r '.data.passwords[0]')
|
||||||
|
|
||||||
|
if [ -n "$PASSWORD" ]; then
|
||||||
|
echo "$PASSWORD" | pbcopy
|
||||||
|
echo "$PASSWORD"
|
||||||
|
else
|
||||||
|
echo "Error generating password"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ubar / Menu Bar Apps
|
||||||
|
|
||||||
|
#### BitBar Plugin
|
||||||
|
|
||||||
|
**password-generator.1m.sh:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "🔐 Password Generator"
|
||||||
|
echo "---"
|
||||||
|
|
||||||
|
# Generate different types
|
||||||
|
TYPES=("high-entropy:High Entropy" "diceware:Diceware" "passkey:Passkey" "xkcdpass:XKCD")
|
||||||
|
|
||||||
|
for TYPE_PAIR in "${TYPES[@]}"; do
|
||||||
|
TYPE="${TYPE_PAIR%%:*}"
|
||||||
|
NAME="${TYPE_PAIR##*:}"
|
||||||
|
|
||||||
|
PASSWORD=$(curl -s -X POST http://localhost:8080/api/v1/passwords/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"type\":\"$TYPE\",\"count\":1}" | \
|
||||||
|
jq -r '.data.passwords[0]' 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$PASSWORD" ]; then
|
||||||
|
echo "$NAME: $PASSWORD | bash='echo \"$PASSWORD\" | pbcopy' terminal=false"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "---"
|
||||||
|
echo "API Status | bash='curl -s http://localhost:8080/api/v1/health' terminal=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Automation Scripts
|
||||||
|
|
||||||
|
### Automated Password Rotation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# rotate-passwords.sh
|
||||||
|
|
||||||
|
SERVICES=("email:user@example.com" "bank:john.doe" "social:johndoe123")
|
||||||
|
|
||||||
|
for SERVICE_PAIR in "${SERVICES[@]}"; do
|
||||||
|
SERVICE="${SERVICE_PAIR%%:*}"
|
||||||
|
USERNAME="${SERVICE_PAIR##*:}"
|
||||||
|
|
||||||
|
# Generate new password
|
||||||
|
PASSWORD=$(curl -s -X POST http://localhost:8080/api/v1/passwords/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"type":"high-entropy","count":1,"options":{"length":20}}' | \
|
||||||
|
jq -r '.data.passwords[0]')
|
||||||
|
|
||||||
|
# Update in password manager (example with Bitwarden)
|
||||||
|
ITEM_ID=$(bw list items --search "$SERVICE" | jq -r '.[0].id')
|
||||||
|
if [ "$ITEM_ID" != "null" ]; then
|
||||||
|
ITEM=$(bw get item "$ITEM_ID")
|
||||||
|
UPDATED_ITEM=$(echo "$ITEM" | jq ".login.password = \"$PASSWORD\"")
|
||||||
|
echo "$UPDATED_ITEM" | bw encode | bw edit item "$ITEM_ID"
|
||||||
|
|
||||||
|
echo "Rotated password for $SERVICE: $PASSWORD"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bulk Password Generation for Teams
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# team-password-generator.sh
|
||||||
|
|
||||||
|
TEAM_MEMBERS=("alice" "bob" "charlie" "diana")
|
||||||
|
SERVICES=("staging" "development" "testing")
|
||||||
|
|
||||||
|
for MEMBER in "${TEAM_MEMBERS[@]}"; do
|
||||||
|
echo "Generating passwords for $MEMBER:"
|
||||||
|
|
||||||
|
for SERVICE in "${SERVICES[@]}"; do
|
||||||
|
PASSWORD=$(curl -s -X POST http://localhost:8080/api/v1/passwords/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"type":"diceware","count":1,"options":{"words":4,"addNumber":true}}' | \
|
||||||
|
jq -r '.data.passwords[0]')
|
||||||
|
|
||||||
|
# Store in team password manager
|
||||||
|
op item create \
|
||||||
|
--category=login \
|
||||||
|
--title="$SERVICE-$MEMBER" \
|
||||||
|
--field="username=$MEMBER" \
|
||||||
|
--field="password=$PASSWORD" \
|
||||||
|
--vault="Team Vault"
|
||||||
|
|
||||||
|
echo " $SERVICE: $PASSWORD"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### API Security
|
||||||
|
|
||||||
|
1. **Local Deployment**: Run the API locally or on a secure internal network
|
||||||
|
2. **Rate Limiting**: The API includes built-in rate limiting (60 requests/minute by default)
|
||||||
|
3. **No Logging of Passwords**: Passwords are not logged in server logs
|
||||||
|
4. **HTTPS**: Use HTTPS in production deployments
|
||||||
|
|
||||||
|
### Integration Security
|
||||||
|
|
||||||
|
1. **Environment Variables**: Store API endpoints in environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PASSWORD_API_URL="https://passwords.internal.company.com/api/v1"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Script Permissions**: Set appropriate permissions on scripts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod 700 password-scripts/
|
||||||
|
chmod 600 password-scripts/*.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Secure Storage**: Ensure integration scripts don't leave passwords in temporary files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Good: Use pipes and variables
|
||||||
|
PASSWORD=$(curl -s ... | jq -r '.data.passwords[0]')
|
||||||
|
|
||||||
|
# Bad: Don't write to temporary files
|
||||||
|
# curl -s ... > /tmp/password.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Deployment
|
||||||
|
|
||||||
|
For production use, consider:
|
||||||
|
|
||||||
|
1. **Authentication**: Add API key authentication
|
||||||
|
2. **Network Security**: Deploy behind a firewall or VPN
|
||||||
|
3. **Monitoring**: Monitor API usage and failed requests
|
||||||
|
4. **Backup**: Regular backups of configuration and logs
|
||||||
|
|
||||||
|
### Password Manager Sync
|
||||||
|
|
||||||
|
When integrating with password managers:
|
||||||
|
|
||||||
|
1. **Verify Storage**: Always verify passwords are correctly stored
|
||||||
|
2. **Error Handling**: Implement proper error handling for API failures
|
||||||
|
3. **Rollback**: Have procedures to rollback failed password updates
|
||||||
|
4. **Audit Trail**: Maintain logs of password generation and updates
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**API Connection Issues:**
|
||||||
|
```bash
|
||||||
|
# Test API connectivity
|
||||||
|
curl -s http://localhost:8080/api/v1/health
|
||||||
|
|
||||||
|
# Check if service is running
|
||||||
|
netstat -tlnp | grep 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
**JSON Parsing Issues:**
|
||||||
|
```bash
|
||||||
|
# Validate JSON response
|
||||||
|
curl -s http://localhost:8080/api/v1/passwords | jq '.'
|
||||||
|
|
||||||
|
# Check for errors
|
||||||
|
curl -s http://localhost:8080/api/v1/passwords | jq '.error'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Password Manager Integration:**
|
||||||
|
```bash
|
||||||
|
# Test CLI tools separately
|
||||||
|
bw --version
|
||||||
|
op --version
|
||||||
|
keepassxc-cli --version
|
||||||
|
|
||||||
|
# Verify authentication
|
||||||
|
bw status
|
||||||
|
op whoami
|
||||||
|
```
|
||||||
|
|
||||||
|
This integration guide provides comprehensive examples for connecting the Password Generator API with popular tools and services. Adapt these examples based on your specific requirements and security policies.
|
||||||
+223
@@ -0,0 +1,223 @@
|
|||||||
|
{
|
||||||
|
"api_documentation": "API-Dokumentation",
|
||||||
|
"app_title": "Passwort Generator",
|
||||||
|
"close": "Schließen",
|
||||||
|
"copied_to_clipboard": "In die Zwischenablage kopiert!",
|
||||||
|
"copy": "Kopieren",
|
||||||
|
"copy_failed": "Kopieren fehlgeschlagen",
|
||||||
|
"count": "Anzahl",
|
||||||
|
"deutsch": "Deutsch",
|
||||||
|
"dice_rolls": "Würfelwürfe",
|
||||||
|
"diceware_badge": "Fokus Sicherheit",
|
||||||
|
"diceware_description": "Authentische EFF Diceware-Methode mit kryptographisch sicheren Würfelwürfen für maximale Sicherheit",
|
||||||
|
"diceware_option_show_dice_rolls": "Zeige Würfelwürfe an",
|
||||||
|
"diceware_option_show_dice_rolls_description": "Informativ: zeige die Würfelwürfe an, welche die Basis für die Auswahl der Wörter ist.",
|
||||||
|
"diceware_option_uppercase_first": "Grossschreibung des ersten Wortes",
|
||||||
|
"diceware_option_uppercase_first_description": "Nur das erste Wort wird gross geschrieben (traditionelle Diceware Mechanik)",
|
||||||
|
"diceware_option_words": "Wörter",
|
||||||
|
"diceware_option_words_description": "Das empfohlene Minimum sind 6 Wörter um ein sicheres Passwort zu erhalten (77.5 Bits Entropie).",
|
||||||
|
"diceware_title": "Diceware",
|
||||||
|
"english": "Englisch",
|
||||||
|
"entropy": "Entropie",
|
||||||
|
"length": "Länge",
|
||||||
|
"details": "Details",
|
||||||
|
"security_level": "Sicherheitsstufe",
|
||||||
|
"very_strong": "Sehr stark",
|
||||||
|
"strong": "Stark",
|
||||||
|
"adequate": "Ausreichend",
|
||||||
|
"weak": "Schwach",
|
||||||
|
"very_weak": "Sehr schwach",
|
||||||
|
"chars": "Zeichen",
|
||||||
|
"error_generating_passwords": "Fehler beim Generieren der Passwörter",
|
||||||
|
"espanol": "Español",
|
||||||
|
"footer_text": "Ein Werkzeug um sichere Passwörter mit verschiedenen Algorithmen zu erstellen. Open Source von Reindl Informatik.",
|
||||||
|
"francais": "Français",
|
||||||
|
"generate": "Generieren",
|
||||||
|
"generating": "Generiere...",
|
||||||
|
"high-entropy_description": "Kryptographisch sichere Zufallspasswörter mit maximaler Entropie",
|
||||||
|
"high-entropy_option_include_lowercase": "Kleinbuchstaben einbeziehen",
|
||||||
|
"high-entropy_option_include_numbers": "Zahlen einbeziehen",
|
||||||
|
"high-entropy_option_include_symbols": "Sonderzeichen einbeziehen",
|
||||||
|
"high-entropy_option_include_uppercase": "Grossbuchstaben einbeziehen",
|
||||||
|
"high-entropy_option_length": "Länge",
|
||||||
|
"high-entropy_title": "Hohe Entropie",
|
||||||
|
"info_spaces_replaced_with_underscores": "Leerzeichen in ausgewählten Wörtern werden durch Unterstriche (_) ersetzt.",
|
||||||
|
"info_words_with_spaces_excluded": "Wörter mit Leerzeichen werden von der Auswahl ausgeschlossen.",
|
||||||
|
"italiano": "italienisch",
|
||||||
|
"language": "Sprache:",
|
||||||
|
"loading_generators": "Generatoren laden …",
|
||||||
|
"loading_password_generators": "Passwort-Generatoren werden geladen...",
|
||||||
|
"native_group_explanation": "Native Optionen des PwGen-Generators zum Erzeugen aussprachefähiger Passwörter.",
|
||||||
|
"native_pwgen": "Native Optionen",
|
||||||
|
"no_spaces_in_separator": "Keine Leerzeichen im Trennzeichen verwenden, wenn die Leerzeichenbehandlung auf „ändern“ oder „überspringen“ gesetzt ist.",
|
||||||
|
"passkey_description": "Apple-Style aussprechbare Passwörter mit integrierten Zeichenersetzungen",
|
||||||
|
"passkey_option_syllables": "Silben",
|
||||||
|
"passkey_title": "Passkey",
|
||||||
|
"password_generator": "Passwort-Generator",
|
||||||
|
"php_fallback": "PHP-Fallback-Code, wenn native pwgen nicht verfügbar ist.",
|
||||||
|
"portugues": "portugiesisch",
|
||||||
|
"pwgen_badge": "Erweitert",
|
||||||
|
"pwgen_description": "Klassische pwgen-Style Passwörter mit lesbaren Mustern",
|
||||||
|
"pwgen_option_include_capitals": "Großbuchstaben einschließen",
|
||||||
|
"pwgen_option_include_capitals_description": "Generierte Passwörter enthalten Großbuchstaben.",
|
||||||
|
"pwgen_option_include_numbers": "Zahlen einschließen",
|
||||||
|
"pwgen_option_include_numbers_description": "Generierte Passwörter enthalten Ziffern.",
|
||||||
|
"pwgen_option_length": "Länge",
|
||||||
|
"pwgen_option_length_description": "Gesamtpasswortlänge einschließlich Trennzeichen (mindestens 4 Zeichen)",
|
||||||
|
"pwgen_option_no_ambiguous": "Keine mehrdeutigen Zeichen",
|
||||||
|
"pwgen_option_no_ambiguous_description": "Schließe Zeichen wie 0, O, l, 1 usw. aus, um die Lesbarkeit zu verbessern",
|
||||||
|
"pwgen_option_pattern_complexity": "Komplexes Muster",
|
||||||
|
"pwgen_option_pattern_complexity_description": "Steuert die Komplexität der erzeugten Muster.",
|
||||||
|
"pwgen_option_pattern_complexity_list_balanced": "Ausgewogene Komplexität",
|
||||||
|
"pwgen_option_pattern_complexity_list_complex": "Komplexe Muster",
|
||||||
|
"pwgen_option_pattern_complexity_list_simple": "Einfache Muster",
|
||||||
|
"pwgen_option_secure_mode": "Sicherer Modus",
|
||||||
|
"pwgen_option_secure_mode_description": "Erzeuge vollständig zufällige Passwörter statt aussprechbarer Wörter",
|
||||||
|
"pwgen_option_separator": "Trennzeichen",
|
||||||
|
"pwgen_option_separator_description": "Zeichen, die Silben trennen. Wenn mehrere Zeichen definiert sind, werden sie durch zufällig ausgewählte Zeichen getrennt. Wenn dies leer ist, findet keine Trennung statt.",
|
||||||
|
"pwgen_option_syllable_count": "Silbenanzahl",
|
||||||
|
"pwgen_option_syllable_count_description": "Anzahl der zu erzeugenden Silben (nur mit Trennzeichen verwendet)",
|
||||||
|
"pwgen_title": "PWGen",
|
||||||
|
"results": "Ergebnisse",
|
||||||
|
"schwyzerduetsch": "Schwyzerdütsch",
|
||||||
|
"secure_mode_disables_syllable_separation": "Sicherer Modus deaktiviert die Trennung von Silben.",
|
||||||
|
"separator_method_info": "Infos zur Trennmethode",
|
||||||
|
"single_space_allowed": "Einzelnes Leerzeichen erlaubt",
|
||||||
|
"success": "Erfolg",
|
||||||
|
"syllable_group_explanation": "Erklärungen zu Silben",
|
||||||
|
"syllable_length_too_short": "Silbenlänge zu kurz",
|
||||||
|
"syllable_length_very_long": "Silbenlänge sehr lang",
|
||||||
|
"warning_short_word_limits_reduces_choices": "Warnung: kurze Wortlimits verringern die Auswahlmöglichkeiten",
|
||||||
|
"warning_short_words_reduce_memorability": "Warnung: kurze Wörter verringern die Merkbarkeit",
|
||||||
|
"xkcdpass_badge": "Fokus Merkbarkeit",
|
||||||
|
"xkcdpass_description": "Einfach merkbare Passwörter aus Wortkombinationen aus auswählbaren Wortlisten mit zusätzlichen Optionen und Merkhilfen.",
|
||||||
|
"xkcdpass_option_addNumber": "Zahl hinzufügen",
|
||||||
|
"xkcdpass_option_max_word_length": "Maximale Wortlänge",
|
||||||
|
"xkcdpass_option_max_word_length_description": "0 = keine Einschränkung, >0 = maximale Anzahl Buchstaben pro Wort.",
|
||||||
|
"xkcdpass_option_numberPosition": "Zahlenposition",
|
||||||
|
"xkcdpass_option_numberPosition_list_beginning": "am Anfang",
|
||||||
|
"xkcdpass_option_numberPosition_list_end": "am Ende",
|
||||||
|
"xkcdpass_option_numberPosition_list_random": "zufällige Position",
|
||||||
|
"xkcdpass_option_separator": "Trennzeichen",
|
||||||
|
"xkcdpass_option_separator_description": "Ein einzelnes oder mehrere Zeichen zur Worttrennung. Bei mehr als einem Zeichen wird zufällig ausgewählt. Leerzeichen werde ignoriert.",
|
||||||
|
"xkcdpass_option_space_handling": "Leerzeichenverarbeitung",
|
||||||
|
"xkcdpass_option_space_handling_description": "Wie sollen Leerzeichen in ausgewählten Wörtern behandelt werden.",
|
||||||
|
"xkcdpass_option_space_handling_list_allow": "erlauben",
|
||||||
|
"xkcdpass_option_space_handling_list_modify": "ersetzen mit '_'",
|
||||||
|
"xkcdpass_option_space_handling_list_skip": "Wort überspringen",
|
||||||
|
"xkcdpass_option_wordlist": "Wortliste",
|
||||||
|
"xkcdpass_option_words": "Wörter",
|
||||||
|
"xkcdpass_option_words_description": "Anzahl Wörter im generierten Passwort (Minimum 2, empfohlen 3 und mehr).",
|
||||||
|
"xkcdpass_title": "XKCD Pass",
|
||||||
|
"phraseabbreviator_number_zero": "null",
|
||||||
|
"phraseabbreviator_number_one": "eins",
|
||||||
|
"phraseabbreviator_number_two": "zwei",
|
||||||
|
"phraseabbreviator_number_three": "drei",
|
||||||
|
"phraseabbreviator_number_four": "vier",
|
||||||
|
"phraseabbreviator_number_five": "fünf",
|
||||||
|
"phraseabbreviator_number_six": "sechs",
|
||||||
|
"phraseabbreviator_number_seven": "sieben",
|
||||||
|
"phraseabbreviator_number_eight": "acht",
|
||||||
|
"phraseabbreviator_number_nine": "neun",
|
||||||
|
"phraseabbreviator_number_ten": "zehn",
|
||||||
|
"phraseabbreviator_number_eleven": "elf",
|
||||||
|
"phraseabbreviator_number_twelve": "zwölf",
|
||||||
|
"phraseabbreviator_number_thirteen": "dreizehn",
|
||||||
|
"phraseabbreviator_number_fourteen": "vierzehn",
|
||||||
|
"phraseabbreviator_number_fifteen": "fünfzehn",
|
||||||
|
"phraseabbreviator_number_sixteen": "sechzehn",
|
||||||
|
"phraseabbreviator_number_seventeen": "siebzehn",
|
||||||
|
"phraseabbreviator_number_eighteen": "achtzehn",
|
||||||
|
"phraseabbreviator_number_nineteen": "neunzehn",
|
||||||
|
"phraseabbreviator_number_twenty": "zwanzig",
|
||||||
|
"xkcdpass_option_use_ai_wordlist": "KI-Wortliste verwenden",
|
||||||
|
"xkcdpass_option_use_ai_wordlist_description": "Erzeugt eine benutzerdefinierte Wortliste mit einem KI-Modell",
|
||||||
|
"xkcdpass_option_ai_wordlist_prompt": "KI-Wortlisten-Prompt",
|
||||||
|
"xkcdpass_option_ai_wordlist_prompt_description": "Vervollständige den Prompt: \"Erstelle eine Liste von X Wörtern mit mindestens 5 Zeichen, die...\"",
|
||||||
|
"xkcdpass_option_ai_provider": "KI-Anbieter",
|
||||||
|
"xkcdpass_option_ai_provider_description": "Welcher LLM-Anbieter verwendet werden soll",
|
||||||
|
"phrase-abbreviator_option_use_ai_sentence": "KI-Satz verwenden",
|
||||||
|
"phrase-abbreviator_option_use_ai_sentence_description": "Lässt die KI einen Satz aus deinem Prompt generieren",
|
||||||
|
"phrase-abbreviator_option_ai_prompt": "KI-Prompt",
|
||||||
|
"phrase-abbreviator_option_ai_prompt_description": "Prompt für die KI, um einen einprägsamen Satz zu generieren (mindestens 12 Wörter)",
|
||||||
|
"phrase-abbreviator_option_ai_provider": "KI-Anbieter",
|
||||||
|
"phrase-abbreviator_option_ai_provider_description": "Welcher LLM-Anbieter für die Satzgenerierung verwendet werden soll",
|
||||||
|
"phrase-abbreviator_option_ai_model": "KI-Modell",
|
||||||
|
"phrase-abbreviator_option_ai_model_description": "Welches KI-Modell verwendet werden soll",
|
||||||
|
"high-entropy_option_length_description": "Gesamtpasswortlänge",
|
||||||
|
"high-entropy_option_include_uppercase_description": "Grossbuchstaben einbeziehen (A-Z)",
|
||||||
|
"high-entropy_option_include_lowercase_description": "Kleinbuchstaben einbeziehen (a-z)",
|
||||||
|
"high-entropy_option_include_numbers_description": "Zahlen einbeziehen (0-9)",
|
||||||
|
"high-entropy_option_include_symbols_description": "Sonderzeichen einbeziehen (z. B. !@#$%)",
|
||||||
|
"xkcdpass_option_wordlist_description": "Wortliste für die Passphrase",
|
||||||
|
"xkcdpass_option_wordlist_choice_en": "Englisch",
|
||||||
|
"xkcdpass_option_wordlist_choice_de": "Deutsch",
|
||||||
|
"xkcdpass_option_wordlist_choice_fr": "Französisch",
|
||||||
|
"xkcdpass_option_wordlist_choice_it": "Italienisch",
|
||||||
|
"xkcdpass_option_wordlist_choice_es": "Spanisch",
|
||||||
|
"xkcdpass_option_space_handling_choice_allow": "Erlauben",
|
||||||
|
"xkcdpass_option_space_handling_choice_skip": "Überspringen",
|
||||||
|
"xkcdpass_option_space_handling_choice_modify": "Ändern",
|
||||||
|
"xkcdpass_option_add_number": "Zahl hinzufügen",
|
||||||
|
"xkcdpass_option_add_number_description": "Zufällige Zahl zur Passphrase hinzufügen",
|
||||||
|
"xkcdpass_option_number_position": "Zahlenposition",
|
||||||
|
"xkcdpass_option_number_position_description": "Wo die Zufallszahl platziert werden soll",
|
||||||
|
"xkcdpass_option_number_position_choice_beginning": "Am Anfang",
|
||||||
|
"xkcdpass_option_number_position_choice_end": "Am Ende",
|
||||||
|
"xkcdpass_option_number_position_choice_random": "Zufällige Position",
|
||||||
|
"xkcdpass_option_ai_provider_choice_openai": "OpenAI",
|
||||||
|
"xkcdpass_option_ai_provider_choice_claude": "Claude",
|
||||||
|
"xkcdpass_option_ai_provider_choice_gemini": "Gemini",
|
||||||
|
"xkcdpass_option_ai_provider_choice_perplexity": "Perplexity",
|
||||||
|
"pwgen_option_type": "Längenvoreinstellung",
|
||||||
|
"pwgen_option_type_description": "Optionale Längenvoreinstellung. Wenn 8/12/16 Zeichen gewählt sind, überschreibt sie den obigen Längenwert.",
|
||||||
|
"pwgen_option_type_choice_8-char": "8 Zeichen",
|
||||||
|
"pwgen_option_type_choice_12-char": "12 Zeichen",
|
||||||
|
"pwgen_option_type_choice_16-char": "16 Zeichen",
|
||||||
|
"passkey_option_syllables_description": "Anzahl der aussprachefähigen Silben im Passkey",
|
||||||
|
"phrase-abbreviator_option_sentence": "Satz",
|
||||||
|
"phrase-abbreviator_option_sentence_description": "Vom Benutzer eingegebener Satz (mindestens 8 Wörter, muss mit Großbuchstaben beginnen)",
|
||||||
|
"phrase-abbreviator_option_character_extraction": "Zeichenerfassung",
|
||||||
|
"phrase-abbreviator_option_character_extraction_description": "Methode zum Extrahieren von Zeichen aus jedem Wort",
|
||||||
|
"phrase-abbreviator_option_character_extraction_choice_single": "Nur erster Buchstabe",
|
||||||
|
"phrase-abbreviator_option_character_extraction_choice_dual": "Erster Buchstabe + nächster Vokal/Konsonant",
|
||||||
|
"phrase-abbreviator_option_language": "Sprache",
|
||||||
|
"phrase-abbreviator_option_language_description": "Sprache für Zahlwortkonvertierung",
|
||||||
|
"phrase-abbreviator_option_language_choice_en": "Englisch",
|
||||||
|
"phrase-abbreviator_option_language_choice_de": "Deutsch",
|
||||||
|
"phrase-abbreviator_option_language_choice_de_CH": "Schwyzerdütsch",
|
||||||
|
"phrase-abbreviator_option_language_choice_fr": "Französisch",
|
||||||
|
"phrase-abbreviator_option_language_choice_it": "Italienisch",
|
||||||
|
"phrase-abbreviator_option_language_choice_es": "Spanisch",
|
||||||
|
"phrase-abbreviator_option_language_choice_pt": "Portugiesisch",
|
||||||
|
"phrase-abbreviator_option_language_choice_ja": "Japanisch",
|
||||||
|
"phrase-abbreviator_option_language_choice_zh": "Chinesisch",
|
||||||
|
"phrase-abbreviator_option_add_number": "Zahl hinzufügen",
|
||||||
|
"phrase-abbreviator_option_add_number_description": "Zufällige Ziffern zum Passwort hinzufügen",
|
||||||
|
"phrase-abbreviator_option_number_digits": "Ziffernanzahl",
|
||||||
|
"phrase-abbreviator_option_number_digits_description": "Wie viele zufällige Ziffern hinzugefügt werden sollen (1-4)",
|
||||||
|
"phrase-abbreviator_option_number_position": "Zahlenposition",
|
||||||
|
"phrase-abbreviator_option_number_position_description": "Zahl vor oder nach dem Passwort platzieren",
|
||||||
|
"phrase-abbreviator_option_number_position_choice_prepend": "Vorher",
|
||||||
|
"phrase-abbreviator_option_number_position_choice_append": "Danach",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_openai": "OpenAI",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_claude": "Claude",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_gemini": "Gemini",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_perplexity": "Perplexity",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-3.5-turbo": "GPT-3.5 Turbo",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4": "GPT-4",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4-turbo": "GPT-4 Turbo",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4o": "GPT-4 Omni",
|
||||||
|
"collapse_group_basic": "Basisoptionen",
|
||||||
|
"collapse_group_advanced": "Erweiterte Optionen",
|
||||||
|
"collapse_group_ai": "KI-Funktionen",
|
||||||
|
"collapse_group_expert": "Expertenoptionen",
|
||||||
|
"collapse_group_character_set": "Zeichensatz",
|
||||||
|
"copied": "Kopiert",
|
||||||
|
"pwgen_option_type_choice_custom": "Längeneinstellung verwenden",
|
||||||
|
"roulette": "Roulette",
|
||||||
|
"stop": "Stopp",
|
||||||
|
"roulette_interval": "Intervall (s)",
|
||||||
|
"roulette_slow_down": "Intervall erhöhen (langsamer)",
|
||||||
|
"roulette_speed_up": "Intervall verringern (schneller)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
{
|
||||||
|
"api_documentation": "API-Dokumentation",
|
||||||
|
"app_title": "Passwort-Generator",
|
||||||
|
"close": "Schliessen",
|
||||||
|
"copied_to_clipboard": "I d'Zwüscheablag kopiert!",
|
||||||
|
"copy": "Kopierä",
|
||||||
|
"copy_failed": "Kopierä fählgschlage",
|
||||||
|
"count": "Azahl",
|
||||||
|
"deutsch": "Deutsch",
|
||||||
|
"dice_rolls": "Würfelwürf",
|
||||||
|
"diceware_badge": "Sicherheits-Fokus",
|
||||||
|
"diceware_description": "Authentischi EFF Diceware-Methode mit kryptographisch sicherä Würfelwürf för maximali Sicherheit",
|
||||||
|
"diceware_option_show_dice_rolls": "Show Dice Rolls",
|
||||||
|
"diceware_option_show_dice_rolls_description": "Educational: display the dice rolls used to select each word",
|
||||||
|
"diceware_option_uppercase_first": "Capitalize First Word",
|
||||||
|
"diceware_option_uppercase_first_description": "Capitalize only the first word (traditional Diceware style)",
|
||||||
|
"diceware_option_words": "Words",
|
||||||
|
"diceware_option_words_description": "Minimum 6 words recommended for security (77.5 bits entropy)",
|
||||||
|
"diceware_title": "Diceware",
|
||||||
|
"english": "Englisch",
|
||||||
|
"entropy": "Entropie",
|
||||||
|
"length": "Länge",
|
||||||
|
"details": "Details",
|
||||||
|
"security_level": "Sicherheitsstufe",
|
||||||
|
"very_strong": "Sehr stark",
|
||||||
|
"strong": "Stark",
|
||||||
|
"adequate": "Ausreichend",
|
||||||
|
"weak": "Schwach",
|
||||||
|
"very_weak": "Sehr schwach",
|
||||||
|
"chars": "Zeichen",
|
||||||
|
"error_generating_passwords": "Fähler bim Generierä vo dä Passwörter",
|
||||||
|
"espanol": "Español",
|
||||||
|
"footer_text": "Footertext",
|
||||||
|
"francais": "Français",
|
||||||
|
"generate": "Generierä",
|
||||||
|
"generating": "Generierä...",
|
||||||
|
"high-entropy_description": "Kryptographisch sicheri Zuefallspasswörter mit maximalär Entropie",
|
||||||
|
"high-entropy_option_include_lowercase": "Include Lowercase",
|
||||||
|
"high-entropy_option_include_numbers": "Include Numbers",
|
||||||
|
"high-entropy_option_include_symbols": "Include Symbols",
|
||||||
|
"high-entropy_option_include_uppercase": "Include Uppercase",
|
||||||
|
"high-entropy_option_length": "Length",
|
||||||
|
"high-entropy_title": "High Entropy",
|
||||||
|
"info_spaces_replaced_with_underscores": "Leerziiche in gwählte Wörter werde dur Unterstrich (_) ersetzt.",
|
||||||
|
"info_words_with_spaces_excluded": "Wörter mit Leerziiche werde vo de Uswahl usgschlosse.",
|
||||||
|
"italiano": "Italiano",
|
||||||
|
"language": "Sprach:",
|
||||||
|
"loading_generators": "Generatorä wärded glade",
|
||||||
|
"loading_password_generators": "Passwort-Generatorä wärded glade...",
|
||||||
|
"native_group_explanation": "Native Uswahloptionä vom PwGen-Generator für verständlichi Passwörter.",
|
||||||
|
"native_pwgen": "Native Optionen",
|
||||||
|
"no_spaces_in_separator": "Kei Leerziiche im Trennzeiche verwende, wenn d Leerziichebehandlig uf „ändere“ oder „überspringe“ gsetzt isch.",
|
||||||
|
"passkey_description": "Apple-Style ussprechbari Passwörter mit integriertene Zeichenersetzige",
|
||||||
|
"passkey_option_syllables": "Syllables",
|
||||||
|
"passkey_title": "Passkey",
|
||||||
|
"password_generator": "Passwort-Generator",
|
||||||
|
"php_fallback": "PHP-Fallback-Code, wenn native pwgen nöd verfügbar isch.",
|
||||||
|
"portugues": "Portugiesisch",
|
||||||
|
"pwgen_badge": "Verbessert",
|
||||||
|
"pwgen_description": "Klassischi pwgen-Style Passwörter mit läsbari Muschter",
|
||||||
|
"pwgen_option_include_capitals": "Grossbuchstäbe einbeziehe",
|
||||||
|
"pwgen_option_include_capitals_description": "Generierti Passwörter enthalte Grossbuchstäbe.",
|
||||||
|
"pwgen_option_include_numbers": "Zahle einbeziehe",
|
||||||
|
"pwgen_option_include_numbers_description": "Generierti Passwörter enthalte Zifferä.",
|
||||||
|
"pwgen_option_length": "Länge",
|
||||||
|
"pwgen_option_length_description": "Total Passwortlänge inklusive Trennzeiche (mindescht 4 Zeichen)",
|
||||||
|
"pwgen_option_no_ambiguous": "Kei mehrdeutigi Zeichen",
|
||||||
|
"pwgen_option_no_ambiguous_description": "Zeiche wie 0, O, l, 1 usw. usschliesse, damit sie besser lesbar sind",
|
||||||
|
"pwgen_option_pattern_complexity": "Komplexes Muster",
|
||||||
|
"pwgen_option_pattern_complexity_description": "Steuert d Komplexität vo de generierte Muster.",
|
||||||
|
"pwgen_option_pattern_complexity_list_balanced": "Ausgewogene Komplexität",
|
||||||
|
"pwgen_option_pattern_complexity_list_complex": "Komplexi Muster",
|
||||||
|
"pwgen_option_pattern_complexity_list_simple": "Einfachi Muster",
|
||||||
|
"pwgen_option_secure_mode": "Sicherer Modus",
|
||||||
|
"pwgen_option_secure_mode_description": "Vollständig zufälligi Passwörter statt verständlichi Wörter generiere",
|
||||||
|
"pwgen_option_separator": "Trennzeiche",
|
||||||
|
"pwgen_option_separator_description": "Zeichen, mit dene Silbe getrennt werde. Wenn mehre Zeichen definiert sind, werde si dur zufällig gwählti Zeichen getrennt. Wenn leer, findet kei Trennung statt.",
|
||||||
|
"pwgen_option_syllable_count": "Silbezahl",
|
||||||
|
"pwgen_option_syllable_count_description": "Zahl vo de zu generierende Silbe (nur mit Trennzeiche verwendet)",
|
||||||
|
"pwgen_title": "PWGen",
|
||||||
|
"results": "Resultate",
|
||||||
|
"schwyzerduetsch": "Schwizerdütsch",
|
||||||
|
"secure_mode_disables_syllable_separation": "Sicherer Modus deaktiviert d Silbe-Trennung.",
|
||||||
|
"separator_method_info": "Info zur Trennmethode",
|
||||||
|
"single_space_allowed": "Einzel Leerziiche erlaubt",
|
||||||
|
"success": "Erfolg",
|
||||||
|
"syllable_group_explanation": "Erklärig zu Silbe",
|
||||||
|
"syllable_length_too_short": "Silbenlänge z kurz",
|
||||||
|
"syllable_length_very_long": "Silbenlänge sehr lang",
|
||||||
|
"warning_short_word_limits_reduces_choices": "Warnig: churi Wortlimit reducere d Uswahl",
|
||||||
|
"warning_short_words_reduce_memorability": "Warnig: churi Wörter verringe d Merkbarkeit",
|
||||||
|
"xkcdpass_badge": "Märkbarkeits-Fokus",
|
||||||
|
"xkcdpass_description": "xkcdpass-Beschriibig",
|
||||||
|
"xkcdpass_option_addNumber": "Add Number",
|
||||||
|
"xkcdpass_option_max_word_length": "Maximal Längi",
|
||||||
|
"xkcdpass_option_max_word_length_description": "0 = no limit, >0 = maximum characters per word",
|
||||||
|
"xkcdpass_option_numberPosition": "Number Position",
|
||||||
|
"xkcdpass_option_numberPosition_list_beginning": "Am Afang",
|
||||||
|
"xkcdpass_option_numberPosition_list_end": "Am Ente",
|
||||||
|
"xkcdpass_option_numberPosition_list_random": "Zufälligi Position",
|
||||||
|
"xkcdpass_option_separator": "Trennzeiche",
|
||||||
|
"xkcdpass_option_separator_description": "Single character or character set (random selection). Spaces excluded from multi-character sets.",
|
||||||
|
"xkcdpass_option_space_handling": "Leerziiche behandle",
|
||||||
|
"xkcdpass_option_space_handling_description": "Behandlig vo Wörter mit Zwüschehräum",
|
||||||
|
"xkcdpass_option_space_handling_list_allow": "Erlaube (Leerziiche behalte)",
|
||||||
|
"xkcdpass_option_space_handling_list_modify": "Ändere (dur _ ersetze)",
|
||||||
|
"xkcdpass_option_space_handling_list_skip": "Überspringe (nit wähle)",
|
||||||
|
"xkcdpass_option_wordlist": "Wörterliste",
|
||||||
|
"xkcdpass_option_words": "Wörter",
|
||||||
|
"xkcdpass_option_words_description": "Number of words (minimal 2)",
|
||||||
|
"xkcdpass_title": "XKCD Pass",
|
||||||
|
"phraseabbreviator_number_zero": "null",
|
||||||
|
"phraseabbreviator_number_one": "eins",
|
||||||
|
"phraseabbreviator_number_two": "zwei",
|
||||||
|
"phraseabbreviator_number_three": "drei",
|
||||||
|
"phraseabbreviator_number_four": "vier",
|
||||||
|
"phraseabbreviator_number_five": "fünf",
|
||||||
|
"phraseabbreviator_number_six": "sechs",
|
||||||
|
"phraseabbreviator_number_seven": "sieben",
|
||||||
|
"phraseabbreviator_number_eight": "acht",
|
||||||
|
"phraseabbreviator_number_nine": "neun",
|
||||||
|
"phraseabbreviator_number_ten": "zehn",
|
||||||
|
"phraseabbreviator_number_eleven": "elf",
|
||||||
|
"phraseabbreviator_number_twelve": "zwölf",
|
||||||
|
"phraseabbreviator_number_thirteen": "dreizehn",
|
||||||
|
"phraseabbreviator_number_fourteen": "vierzehn",
|
||||||
|
"phraseabbreviator_number_fifteen": "fünfzehn",
|
||||||
|
"phraseabbreviator_number_sixteen": "sechzehn",
|
||||||
|
"phraseabbreviator_number_seventeen": "siebzehn",
|
||||||
|
"phraseabbreviator_number_eighteen": "achtzehn",
|
||||||
|
"phraseabbreviator_number_nineteen": "neunzehn",
|
||||||
|
"phraseabbreviator_number_twenty": "zwanzig",
|
||||||
|
"xkcdpass_option_use_ai_wordlist": "KI-Wörterliste verwende",
|
||||||
|
"xkcdpass_option_use_ai_wordlist_description": "Erzeig eine personalisierte Wörterliste mit em KI-Modell",
|
||||||
|
"xkcdpass_option_ai_wordlist_prompt": "KI-Wörterliste-Prompt",
|
||||||
|
"xkcdpass_option_ai_wordlist_prompt_description": "Vervollständigi de Prompt: \"Erstell e List vo X Wörter mit mindescht 5 Zeichen, wo...\"",
|
||||||
|
"xkcdpass_option_ai_provider": "KI-Anbieter",
|
||||||
|
"xkcdpass_option_ai_provider_description": "Welcher LLM-Anbieter verwendet werde soll",
|
||||||
|
"phrase-abbreviator_option_use_ai_sentence": "KI-Satz verwende",
|
||||||
|
"phrase-abbreviator_option_use_ai_sentence_description": "Lass d KI en Satz us dim Prompt generiere",
|
||||||
|
"phrase-abbreviator_option_ai_prompt": "KI-Prompt",
|
||||||
|
"phrase-abbreviator_option_ai_prompt_description": "Prompt für d KI, um en einprägsame Satz z generiere (mindescht 12 Wörter)",
|
||||||
|
"phrase-abbreviator_option_ai_provider": "KI-Anbieter",
|
||||||
|
"phrase-abbreviator_option_ai_provider_description": "Welcher LLM-Anbieter für d Satzgenerierig verwendet werde soll",
|
||||||
|
"phrase-abbreviator_option_ai_model": "KI-Modell",
|
||||||
|
"phrase-abbreviator_option_ai_model_description": "Welches KI-Modell verwendet werde soll",
|
||||||
|
"high-entropy_option_length_description": "Total Passwortlänge",
|
||||||
|
"high-entropy_option_include_uppercase_description": "Grossbuchstaabe einbeziege (A-Z)",
|
||||||
|
"high-entropy_option_include_lowercase_description": "Chliibuechsstaabe einbeziege (a-z)",
|
||||||
|
"high-entropy_option_include_numbers_description": "Zifferä einbeziege (0-9)",
|
||||||
|
"high-entropy_option_include_symbols_description": "Sonderzeiche einbeziege (z. B. !@#$%)",
|
||||||
|
"xkcdpass_option_wordlist_description": "Wörterliste für d Passphrase",
|
||||||
|
"xkcdpass_option_wordlist_choice_en": "Englisch",
|
||||||
|
"xkcdpass_option_wordlist_choice_de": "Deutsch",
|
||||||
|
"xkcdpass_option_wordlist_choice_fr": "Französisch",
|
||||||
|
"xkcdpass_option_wordlist_choice_it": "Italienisch",
|
||||||
|
"xkcdpass_option_wordlist_choice_es": "Spanisch",
|
||||||
|
"xkcdpass_option_space_handling_choice_allow": "Erlaube",
|
||||||
|
"xkcdpass_option_space_handling_choice_skip": "Überspringe",
|
||||||
|
"xkcdpass_option_space_handling_choice_modify": "Ändere",
|
||||||
|
"xkcdpass_option_add_number": "Zahl hinzuefüege",
|
||||||
|
"xkcdpass_option_add_number_description": "Zufälligi Zahl zur Passphrase hinzuefüege",
|
||||||
|
"xkcdpass_option_number_position": "Zahlposition",
|
||||||
|
"xkcdpass_option_number_position_description": "Wo d zufälligi Zahl platziert werde söll",
|
||||||
|
"xkcdpass_option_number_position_choice_beginning": "Am Afang",
|
||||||
|
"xkcdpass_option_number_position_choice_end": "Am Ände",
|
||||||
|
"xkcdpass_option_number_position_choice_random": "Zufälligi Position",
|
||||||
|
"xkcdpass_option_ai_provider_choice_openai": "OpenAI",
|
||||||
|
"xkcdpass_option_ai_provider_choice_claude": "Claude",
|
||||||
|
"xkcdpass_option_ai_provider_choice_gemini": "Gemini",
|
||||||
|
"xkcdpass_option_ai_provider_choice_perplexity": "Perplexity",
|
||||||
|
"pwgen_option_type": "Längi-Voreistellig",
|
||||||
|
"pwgen_option_type_description": "Optionali Längi-Voreistellig. Wenn 8/12/16 Zeiche gwählt sind, überschriibt sie de obigi Längiwärt.",
|
||||||
|
"pwgen_option_type_choice_8-char": "8 Zeichen",
|
||||||
|
"pwgen_option_type_choice_12-char": "12 Zeichen",
|
||||||
|
"pwgen_option_type_choice_16-char": "16 Zeichen",
|
||||||
|
"passkey_option_syllables_description": "Zahl vo de verständliche Silbe im Passkey",
|
||||||
|
"phrase-abbreviator_option_sentence": "Satz",
|
||||||
|
"phrase-abbreviator_option_sentence_description": "Vom Benutzer igäh Satz (mindescht 8 Wörter, muess mit Grossbuächstaabe afange)",
|
||||||
|
"phrase-abbreviator_option_character_extraction": "Zeichenextraktion",
|
||||||
|
"phrase-abbreviator_option_character_extraction_description": "Methode zum Extrahiere vo Zeiche us jedem Wort",
|
||||||
|
"phrase-abbreviator_option_character_extraction_choice_single": "Nur erster Buächstab",
|
||||||
|
"phrase-abbreviator_option_character_extraction_choice_dual": "Erster Buächstab + nächster Vokal/Konsonant",
|
||||||
|
"phrase-abbreviator_option_language": "Sproch",
|
||||||
|
"phrase-abbreviator_option_language_description": "Sproch für Ziffere-Wort-Umsetzung",
|
||||||
|
"phrase-abbreviator_option_language_choice_en": "Englisch",
|
||||||
|
"phrase-abbreviator_option_language_choice_de": "Deutsch",
|
||||||
|
"phrase-abbreviator_option_language_choice_de_CH": "Schwyzerdütsch",
|
||||||
|
"phrase-abbreviator_option_language_choice_fr": "Französisch",
|
||||||
|
"phrase-abbreviator_option_language_choice_it": "Italienisch",
|
||||||
|
"phrase-abbreviator_option_language_choice_es": "Spanisch",
|
||||||
|
"phrase-abbreviator_option_language_choice_pt": "Portugiesisch",
|
||||||
|
"phrase-abbreviator_option_language_choice_ja": "Japanisch",
|
||||||
|
"phrase-abbreviator_option_language_choice_zh": "Chinesisch",
|
||||||
|
"phrase-abbreviator_option_add_number": "Zahl hinzuefüege",
|
||||||
|
"phrase-abbreviator_option_add_number_description": "Zufälligi Zifferä zum Passwort hinzuefüege",
|
||||||
|
"phrase-abbreviator_option_number_digits": "Zifferäzahl",
|
||||||
|
"phrase-abbreviator_option_number_digits_description": "Wieviel zufälligi Zifferä solle hinzugefügt werde (1-4)",
|
||||||
|
"phrase-abbreviator_option_number_position": "Zahlposition",
|
||||||
|
"phrase-abbreviator_option_number_position_description": "Zahl vor oder nach em Passwort platziere",
|
||||||
|
"phrase-abbreviator_option_number_position_choice_prepend": "Vorher",
|
||||||
|
"phrase-abbreviator_option_number_position_choice_append": "Darnach",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_openai": "OpenAI",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_claude": "Claude",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_gemini": "Gemini",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_perplexity": "Perplexity",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-3.5-turbo": "GPT-3.5 Turbo",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4": "GPT-4",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4-turbo": "GPT-4 Turbo",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4o": "GPT-4 Omni",
|
||||||
|
"collapse_group_basic": "Basisoptione",
|
||||||
|
"collapse_group_advanced": "Erwiitereti Optione",
|
||||||
|
"collapse_group_ai": "KI-Funktione",
|
||||||
|
"collapse_group_expert": "Experte-Optione",
|
||||||
|
"collapse_group_character_set": "Zeichesatz",
|
||||||
|
"copied": "Kopiert",
|
||||||
|
"pwgen_option_type_choice_custom": "Längi-Istellig verwände",
|
||||||
|
"roulette": "Roulette",
|
||||||
|
"stop": "Stopp",
|
||||||
|
"roulette_interval": "Intervall (s)",
|
||||||
|
"roulette_slow_down": "Intervall erhöche (langsamer)",
|
||||||
|
"roulette_speed_up": "Intervall verringerä (schneller)"
|
||||||
|
}
|
||||||
+223
@@ -0,0 +1,223 @@
|
|||||||
|
{
|
||||||
|
"api_documentation": "API Documentation",
|
||||||
|
"app_title": "Password Generator",
|
||||||
|
"close": "close",
|
||||||
|
"copied_to_clipboard": "Copied to clipboard!",
|
||||||
|
"copy": "Copy",
|
||||||
|
"copy_failed": "Copy failed",
|
||||||
|
"count": "Count",
|
||||||
|
"deutsch": "Deutsch",
|
||||||
|
"dice_rolls": "Dice Rolls",
|
||||||
|
"diceware_badge": "Security Focus",
|
||||||
|
"diceware_description": "Authentic EFF Diceware method using cryptographically secure dice rolls for maximum security",
|
||||||
|
"diceware_option_show_dice_rolls": "Show Dice Rolls",
|
||||||
|
"diceware_option_show_dice_rolls_description": "Educational: display the dice rolls used to select each word",
|
||||||
|
"diceware_option_uppercase_first": "Capitalize First Word",
|
||||||
|
"diceware_option_uppercase_first_description": "Capitalize only the first word (traditional Diceware style)",
|
||||||
|
"diceware_option_words": "Words",
|
||||||
|
"diceware_option_words_description": "Minimum 6 words recommended for recommended security.",
|
||||||
|
"diceware_title": "Diceware",
|
||||||
|
"english": "English",
|
||||||
|
"entropy": "Entropy",
|
||||||
|
"length": "Length",
|
||||||
|
"details": "Details",
|
||||||
|
"security_level": "Security level",
|
||||||
|
"very_strong": "Very strong",
|
||||||
|
"strong": "Strong",
|
||||||
|
"adequate": "Adequate",
|
||||||
|
"weak": "Weak",
|
||||||
|
"very_weak": "Very weak",
|
||||||
|
"chars": "chars",
|
||||||
|
"error_generating_passwords": "Error generating passwords",
|
||||||
|
"espanol": "Español",
|
||||||
|
"footer_text": "A tool to generate secure password with various algorithms. Source code available on GitHub.",
|
||||||
|
"francais": "Français",
|
||||||
|
"generate": "Generate",
|
||||||
|
"generating": "Generating …",
|
||||||
|
"high-entropy_description": "Cryptographically secure random passwords with maximum entropy",
|
||||||
|
"high-entropy_option_include_lowercase": "Include Lowercase",
|
||||||
|
"high-entropy_option_include_numbers": "Include Numbers",
|
||||||
|
"high-entropy_option_include_symbols": "Include Symbols",
|
||||||
|
"high-entropy_option_include_uppercase": "Include Uppercase",
|
||||||
|
"high-entropy_option_length": "Length",
|
||||||
|
"high-entropy_title": "High Entropy",
|
||||||
|
"info_spaces_replaced_with_underscores": "Spaces in selected words are replaced with underscores (_).",
|
||||||
|
"info_words_with_spaces_excluded": "Words containing spaces are excluded from selection.",
|
||||||
|
"italiano": "Italiano",
|
||||||
|
"language": "Language:",
|
||||||
|
"loading_generators": "Loading generators …",
|
||||||
|
"loading_password_generators": "Loading password generators …",
|
||||||
|
"native_group_explanation": "PwGen native generator's options for generating pronounceable passwords.",
|
||||||
|
"native_pwgen": "Native Options",
|
||||||
|
"no_spaces_in_separator": "Don't use spaces in the separator when space handling is set to 'modify' or 'skip'.",
|
||||||
|
"passkey_description": "Apple-style pronounceable passwords with integrated character substitutions",
|
||||||
|
"passkey_option_syllables": "Syllables",
|
||||||
|
"passkey_title": "Passkey",
|
||||||
|
"password_generator": "Password Generator",
|
||||||
|
"php_fallback": "Fallback code in PHP when native pwgen is not available.",
|
||||||
|
"portugues": "Portugues",
|
||||||
|
"pwgen_badge": "Enhanced",
|
||||||
|
"pwgen_description": "Classic pwgen-style passwords with readable patterns",
|
||||||
|
"pwgen_option_include_capitals": "Include Capitals",
|
||||||
|
"pwgen_option_include_capitals_description": "Generated passwords will contain uppercase letters.",
|
||||||
|
"pwgen_option_include_numbers": "Include Numbers",
|
||||||
|
"pwgen_option_include_numbers_description": "Generated passwords will contain digits.",
|
||||||
|
"pwgen_option_length": "Length",
|
||||||
|
"pwgen_option_length_description": "Total password length including separators (minimum 4 characters)",
|
||||||
|
"pwgen_option_no_ambiguous": "No Ambiguous Characters",
|
||||||
|
"pwgen_option_no_ambiguous_description": "Exclude characters like 0, O, l, 1, etc. to improve readability",
|
||||||
|
"pwgen_option_pattern_complexity": "Complex Pattern",
|
||||||
|
"pwgen_option_pattern_complexity_description": "Control the complexity of generated patterns.",
|
||||||
|
"pwgen_option_pattern_complexity_list_balanced": "Balanced complexity",
|
||||||
|
"pwgen_option_pattern_complexity_list_complex": "Complex patterns",
|
||||||
|
"pwgen_option_pattern_complexity_list_simple": "Simple patterns",
|
||||||
|
"pwgen_option_secure_mode": "Secure Mode",
|
||||||
|
"pwgen_option_secure_mode_description": "Generate fully random passwords instead of pronounceable ones",
|
||||||
|
"pwgen_option_separator": "Separator",
|
||||||
|
"pwgen_option_separator_description": "Character(s) to separate syllables. If multiple characters are defined, parts are separate by a randomly selected character. If this is empty, no separation is done.",
|
||||||
|
"pwgen_option_syllable_count": "Syllable Count",
|
||||||
|
"pwgen_option_syllable_count_description": "Number of syllables to generate (only used with separator)",
|
||||||
|
"pwgen_title": "PwGen",
|
||||||
|
"results": "Results",
|
||||||
|
"schwyzerduetsch": "Schwyzerdütsch",
|
||||||
|
"secure_mode_disables_syllable_separation": "Secure mode disables syllable separation.",
|
||||||
|
"separator_method_info": "Separator method info",
|
||||||
|
"single_space_allowed": "Single Space Allowed",
|
||||||
|
"success": "Success",
|
||||||
|
"syllable_group_explanation": "syllable_group_explanation",
|
||||||
|
"syllable_length_too_short": "syllable_length_too_short",
|
||||||
|
"syllable_length_very_long": "syllable_length_very_long",
|
||||||
|
"warning_short_word_limits_reduces_choices": "warning_short_word_limits_reduces_choices",
|
||||||
|
"warning_short_words_reduce_memorability": "warning_short_words_reduce_memorability",
|
||||||
|
"xkcdpass_badge": "Memorability Focus",
|
||||||
|
"xkcdpass_description": "Memorable passphrases using word combinations with enhanced filtering and separator options",
|
||||||
|
"xkcdpass_option_addNumber": "Add Number",
|
||||||
|
"xkcdpass_option_max_word_length": "Max Word Length",
|
||||||
|
"xkcdpass_option_max_word_length_description": "0 = no limit, >0 = maximum characters per word",
|
||||||
|
"xkcdpass_option_numberPosition": "Number Position",
|
||||||
|
"xkcdpass_option_numberPosition_list_beginning": "At Beginning",
|
||||||
|
"xkcdpass_option_numberPosition_list_end": "At End",
|
||||||
|
"xkcdpass_option_numberPosition_list_random": "Random Position",
|
||||||
|
"xkcdpass_option_separator": "Separator",
|
||||||
|
"xkcdpass_option_separator_description": "Single character or character set (random selection). Spaces excluded from multi-character sets.",
|
||||||
|
"xkcdpass_option_space_handling": "Space Handling",
|
||||||
|
"xkcdpass_option_space_handling_description": "How to handle words containing spaces",
|
||||||
|
"xkcdpass_option_space_handling_list_allow": "Allow (keep spaces)",
|
||||||
|
"xkcdpass_option_space_handling_list_modify": "Modify (replace with _)",
|
||||||
|
"xkcdpass_option_space_handling_list_skip": "Skip (don\"t select)",
|
||||||
|
"xkcdpass_option_wordlist": "wordlist",
|
||||||
|
"xkcdpass_option_words": "Words",
|
||||||
|
"xkcdpass_option_words_description": "Number of words (minimal 2)",
|
||||||
|
"xkcdpass_title": "xkcdpass",
|
||||||
|
"phraseabbreviator_number_zero": "zero",
|
||||||
|
"phraseabbreviator_number_one": "one",
|
||||||
|
"phraseabbreviator_number_two": "two",
|
||||||
|
"phraseabbreviator_number_three": "three",
|
||||||
|
"phraseabbreviator_number_four": "four",
|
||||||
|
"phraseabbreviator_number_five": "five",
|
||||||
|
"phraseabbreviator_number_six": "six",
|
||||||
|
"phraseabbreviator_number_seven": "seven",
|
||||||
|
"phraseabbreviator_number_eight": "eight",
|
||||||
|
"phraseabbreviator_number_nine": "nine",
|
||||||
|
"phraseabbreviator_number_ten": "ten",
|
||||||
|
"phraseabbreviator_number_eleven": "eleven",
|
||||||
|
"phraseabbreviator_number_twelve": "twelve",
|
||||||
|
"phraseabbreviator_number_thirteen": "thirteen",
|
||||||
|
"phraseabbreviator_number_fourteen": "fourteen",
|
||||||
|
"phraseabbreviator_number_fifteen": "fifteen",
|
||||||
|
"phraseabbreviator_number_sixteen": "sixteen",
|
||||||
|
"phraseabbreviator_number_seventeen": "seventeen",
|
||||||
|
"phraseabbreviator_number_eighteen": "eighteen",
|
||||||
|
"phraseabbreviator_number_nineteen": "nineteen",
|
||||||
|
"phraseabbreviator_number_twenty": "twenty",
|
||||||
|
"xkcdpass_option_use_ai_wordlist": "Use AI Wordlist",
|
||||||
|
"xkcdpass_option_use_ai_wordlist_description": "Generate a custom wordlist using an AI model",
|
||||||
|
"xkcdpass_option_ai_wordlist_prompt": "AI Wordlist Prompt",
|
||||||
|
"xkcdpass_option_ai_wordlist_prompt_description": "Complete the prompt: \"Create a list of X words of at least 5 characters, which...\"",
|
||||||
|
"xkcdpass_option_ai_provider": "AI Provider",
|
||||||
|
"xkcdpass_option_ai_provider_description": "Which LLM provider to use",
|
||||||
|
"phrase-abbreviator_option_use_ai_sentence": "Use AI Sentence",
|
||||||
|
"phrase-abbreviator_option_use_ai_sentence_description": "Let AI generate a sentence from your prompt",
|
||||||
|
"phrase-abbreviator_option_ai_prompt": "AI Prompt",
|
||||||
|
"phrase-abbreviator_option_ai_prompt_description": "Prompt for AI to generate a memorable sentence (minimum 12 words)",
|
||||||
|
"phrase-abbreviator_option_ai_provider": "AI Provider",
|
||||||
|
"phrase-abbreviator_option_ai_provider_description": "Which LLM provider to use for sentence generation",
|
||||||
|
"phrase-abbreviator_option_ai_model": "AI Model",
|
||||||
|
"phrase-abbreviator_option_ai_model_description": "Which AI model to use",
|
||||||
|
"high-entropy_option_length_description": "Total length of the generated password",
|
||||||
|
"high-entropy_option_include_uppercase_description": "Include uppercase letters (A-Z)",
|
||||||
|
"high-entropy_option_include_lowercase_description": "Include lowercase letters (a-z)",
|
||||||
|
"high-entropy_option_include_numbers_description": "Include numbers (0-9)",
|
||||||
|
"high-entropy_option_include_symbols_description": "Include symbols (e.g., !@#$%)",
|
||||||
|
"xkcdpass_option_wordlist_description": "Wordlist to use for generating the passphrase",
|
||||||
|
"xkcdpass_option_wordlist_choice_en": "English",
|
||||||
|
"xkcdpass_option_wordlist_choice_de": "Deutsch",
|
||||||
|
"xkcdpass_option_wordlist_choice_fr": "Français",
|
||||||
|
"xkcdpass_option_wordlist_choice_it": "Italiano",
|
||||||
|
"xkcdpass_option_wordlist_choice_es": "Español",
|
||||||
|
"xkcdpass_option_space_handling_choice_allow": "Allow",
|
||||||
|
"xkcdpass_option_space_handling_choice_skip": "Skip",
|
||||||
|
"xkcdpass_option_space_handling_choice_modify": "Modify",
|
||||||
|
"xkcdpass_option_add_number": "Add Number",
|
||||||
|
"xkcdpass_option_add_number_description": "Add random number to the passphrase",
|
||||||
|
"xkcdpass_option_number_position": "Number Position",
|
||||||
|
"xkcdpass_option_number_position_description": "Where to place the random number",
|
||||||
|
"xkcdpass_option_number_position_choice_beginning": "At Beginning",
|
||||||
|
"xkcdpass_option_number_position_choice_end": "At End",
|
||||||
|
"xkcdpass_option_number_position_choice_random": "Random Position",
|
||||||
|
"xkcdpass_option_ai_provider_choice_openai": "OpenAI",
|
||||||
|
"xkcdpass_option_ai_provider_choice_claude": "Claude",
|
||||||
|
"xkcdpass_option_ai_provider_choice_gemini": "Gemini",
|
||||||
|
"xkcdpass_option_ai_provider_choice_perplexity": "Perplexity",
|
||||||
|
"pwgen_option_type": "Length Preset",
|
||||||
|
"pwgen_option_type_description": "Optional preset length. If set to 8/12/16 characters, it overrides the Length value above.",
|
||||||
|
"pwgen_option_type_choice_8-char": "8 characters",
|
||||||
|
"pwgen_option_type_choice_12-char": "12 characters",
|
||||||
|
"pwgen_option_type_choice_16-char": "16 characters",
|
||||||
|
"passkey_option_syllables_description": "Number of pronounceable syllables in the passkey",
|
||||||
|
"phrase-abbreviator_option_sentence": "Sentence",
|
||||||
|
"phrase-abbreviator_option_sentence_description": "User-supplied sentence (minimum 8 words, must start with capital letter)",
|
||||||
|
"phrase-abbreviator_option_character_extraction": "Character Extraction",
|
||||||
|
"phrase-abbreviator_option_character_extraction_description": "Method for extracting characters from each word",
|
||||||
|
"phrase-abbreviator_option_character_extraction_choice_single": "First letter only",
|
||||||
|
"phrase-abbreviator_option_character_extraction_choice_dual": "First letter + next vowel/consonant",
|
||||||
|
"phrase-abbreviator_option_language": "Language",
|
||||||
|
"phrase-abbreviator_option_language_description": "Language for number word conversion",
|
||||||
|
"phrase-abbreviator_option_language_choice_en": "English",
|
||||||
|
"phrase-abbreviator_option_language_choice_de": "Deutsch",
|
||||||
|
"phrase-abbreviator_option_language_choice_de_CH": "Schwyzerdütsch",
|
||||||
|
"phrase-abbreviator_option_language_choice_fr": "Français",
|
||||||
|
"phrase-abbreviator_option_language_choice_it": "Italiano",
|
||||||
|
"phrase-abbreviator_option_language_choice_es": "Español",
|
||||||
|
"phrase-abbreviator_option_language_choice_pt": "Português",
|
||||||
|
"phrase-abbreviator_option_language_choice_ja": "日本語",
|
||||||
|
"phrase-abbreviator_option_language_choice_zh": "中文",
|
||||||
|
"phrase-abbreviator_option_add_number": "Add Number",
|
||||||
|
"phrase-abbreviator_option_add_number_description": "Add random digits to the password",
|
||||||
|
"phrase-abbreviator_option_number_digits": "Digit Count",
|
||||||
|
"phrase-abbreviator_option_number_digits_description": "How many random digits to add (1-4)",
|
||||||
|
"phrase-abbreviator_option_number_position": "Number Position",
|
||||||
|
"phrase-abbreviator_option_number_position_description": "Place number before or after the password",
|
||||||
|
"phrase-abbreviator_option_number_position_choice_prepend": "Before",
|
||||||
|
"phrase-abbreviator_option_number_position_choice_append": "After",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_openai": "OpenAI",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_claude": "Claude",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_gemini": "Gemini",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_perplexity": "Perplexity",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-3.5-turbo": "GPT-3.5 Turbo",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4": "GPT-4",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4-turbo": "GPT-4 Turbo",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4o": "GPT-4 Omni",
|
||||||
|
"collapse_group_basic": "Basic Options",
|
||||||
|
"collapse_group_advanced": "Advanced Options",
|
||||||
|
"collapse_group_ai": "AI Features",
|
||||||
|
"collapse_group_expert": "Expert Options",
|
||||||
|
"collapse_group_character_set": "Character Set",
|
||||||
|
"copied": "Copied",
|
||||||
|
"pwgen_option_type_choice_custom": "Use length setting",
|
||||||
|
"roulette": "Roulette",
|
||||||
|
"stop": "Stop",
|
||||||
|
"roulette_interval": "Interval (s)",
|
||||||
|
"roulette_slow_down": "Increase interval (slower)",
|
||||||
|
"roulette_speed_up": "Decrease interval (faster)"
|
||||||
|
}
|
||||||
+223
@@ -0,0 +1,223 @@
|
|||||||
|
{
|
||||||
|
"api_documentation": "Documentación de API",
|
||||||
|
"app_title": "app_title",
|
||||||
|
"close": "Cerrar",
|
||||||
|
"copied_to_clipboard": "Copied to clipboard!",
|
||||||
|
"copy": "Copiar",
|
||||||
|
"copy_failed": "Error al copiar",
|
||||||
|
"count": "Count",
|
||||||
|
"deutsch": "Deutsch",
|
||||||
|
"dice_rolls": "Lanzamientos de dados",
|
||||||
|
"diceware_badge": "Enfoque en seguridad",
|
||||||
|
"diceware_description": "diceware description",
|
||||||
|
"diceware_option_show_dice_rolls": "Show Dice Rolls",
|
||||||
|
"diceware_option_show_dice_rolls_description": "Educativo: muestra los lanzamientos de dados usados para seleccionar cada palabra",
|
||||||
|
"diceware_option_uppercase_first": "Capitalize First Word",
|
||||||
|
"diceware_option_uppercase_first_description": "Solo capitaliza la primera palabra (estilo Diceware tradicional)",
|
||||||
|
"diceware_option_words": "Words",
|
||||||
|
"diceware_option_words_description": "Se recomienda un mínimo de 6 palabras para mayor seguridad (77,5 bits de entropía).",
|
||||||
|
"diceware_title": "Diceware",
|
||||||
|
"english": "English",
|
||||||
|
"entropy": "Entropía",
|
||||||
|
"length": "Longitud",
|
||||||
|
"details": "Detalles",
|
||||||
|
"security_level": "Nivel de seguridad",
|
||||||
|
"very_strong": "Muy fuerte",
|
||||||
|
"strong": "Fuerte",
|
||||||
|
"adequate": "Adecuado",
|
||||||
|
"weak": "Débil",
|
||||||
|
"very_weak": "Muy débil",
|
||||||
|
"chars": "caracteres",
|
||||||
|
"error_generating_passwords": "Error al generar contraseñas",
|
||||||
|
"espanol": "Español",
|
||||||
|
"footer_text": "footer_text",
|
||||||
|
"francais": "Français",
|
||||||
|
"generate": "Generar",
|
||||||
|
"generating": "Generating...",
|
||||||
|
"high-entropy_description": "high-entropy description",
|
||||||
|
"high-entropy_option_include_lowercase": "Include Lowercase",
|
||||||
|
"high-entropy_option_include_numbers": "Include Numbers",
|
||||||
|
"high-entropy_option_include_symbols": "Include Symbols",
|
||||||
|
"high-entropy_option_include_uppercase": "Include Uppercase",
|
||||||
|
"high-entropy_option_length": "Length",
|
||||||
|
"high-entropy_title": "High Entropy",
|
||||||
|
"info_spaces_replaced_with_underscores": "Los espacios en las palabras seleccionadas se reemplazan con guiones bajos (_).",
|
||||||
|
"info_words_with_spaces_excluded": "Las palabras que contienen espacios se excluyen de la selección.",
|
||||||
|
"italiano": "Italiano",
|
||||||
|
"language": "Language:",
|
||||||
|
"loading_generators": "loading_generators",
|
||||||
|
"loading_password_generators": "Loading password generators...",
|
||||||
|
"native_group_explanation": "Opciones nativas del generador PwGen para crear contraseñas pronunciables.",
|
||||||
|
"native_pwgen": "Opciones nativas",
|
||||||
|
"no_spaces_in_separator": "No uses espacios en el separador cuando la gestión de espacios esté configurada como “modificar” o “omitir”.",
|
||||||
|
"passkey_description": "passkey description",
|
||||||
|
"passkey_option_syllables": "Syllables",
|
||||||
|
"passkey_title": "Passkey",
|
||||||
|
"password_generator": "Generador de contraseñas",
|
||||||
|
"php_fallback": "Código de respaldo en PHP cuando no está disponible pwgen nativo.",
|
||||||
|
"portugues": "Portugués",
|
||||||
|
"pwgen_badge": "Mejorado",
|
||||||
|
"pwgen_description": "pwgen description",
|
||||||
|
"pwgen_option_include_capitals": "Incluir mayúsculas",
|
||||||
|
"pwgen_option_include_capitals_description": "Las contraseñas generadas contendrán letras mayúsculas.",
|
||||||
|
"pwgen_option_include_numbers": "Incluir números",
|
||||||
|
"pwgen_option_include_numbers_description": "Las contraseñas generadas contendrán dígitos.",
|
||||||
|
"pwgen_option_length": "Longitud",
|
||||||
|
"pwgen_option_length_description": "Longitud total de la contraseña, incluidos los separadores (mínimo 4 caracteres)",
|
||||||
|
"pwgen_option_no_ambiguous": "Sin caracteres ambiguos",
|
||||||
|
"pwgen_option_no_ambiguous_description": "Excluye caracteres como 0, O, l, 1, etc. para mejorar la legibilidad",
|
||||||
|
"pwgen_option_pattern_complexity": "Patrón complejo",
|
||||||
|
"pwgen_option_pattern_complexity_description": "Controla la complejidad de los patrones generados.",
|
||||||
|
"pwgen_option_pattern_complexity_list_balanced": "Complejidad equilibrada",
|
||||||
|
"pwgen_option_pattern_complexity_list_complex": "Patrones complejos",
|
||||||
|
"pwgen_option_pattern_complexity_list_simple": "Patrones simples",
|
||||||
|
"pwgen_option_secure_mode": "Modo seguro",
|
||||||
|
"pwgen_option_secure_mode_description": "Genera contraseñas totalmente aleatorias en lugar de palabras pronunciables",
|
||||||
|
"pwgen_option_separator": "Separador",
|
||||||
|
"pwgen_option_separator_description": "Caracter(es) para separar sílabas. Si se definen varios, se separan con un carácter elegido aleatoriamente. Si está vacío, no se realiza ninguna separación.",
|
||||||
|
"pwgen_option_syllable_count": "Número de sílabas",
|
||||||
|
"pwgen_option_syllable_count_description": "Número de sílabas a generar (solo se usa con separador)",
|
||||||
|
"pwgen_title": "PWGen",
|
||||||
|
"results": "Resultados",
|
||||||
|
"schwyzerduetsch": "Schwyzerdütsch",
|
||||||
|
"secure_mode_disables_syllable_separation": "El modo seguro desactiva la separación por sílabas.",
|
||||||
|
"separator_method_info": "Información del método de separación",
|
||||||
|
"single_space_allowed": "Se permite un solo espacio",
|
||||||
|
"success": "Éxito",
|
||||||
|
"syllable_group_explanation": "Explicación de las sílabas",
|
||||||
|
"syllable_length_too_short": "La sílaba es demasiado corta",
|
||||||
|
"syllable_length_very_long": "La sílaba es muy larga",
|
||||||
|
"warning_short_word_limits_reduces_choices": "Advertencia: los límites cortos de palabras reducen las opciones",
|
||||||
|
"warning_short_words_reduce_memorability": "Advertencia: las palabras cortas reducen la memorización",
|
||||||
|
"xkcdpass_badge": "Enfoque en la memorización",
|
||||||
|
"xkcdpass_description": "xkcdpass description",
|
||||||
|
"xkcdpass_option_addNumber": "Add Number",
|
||||||
|
"xkcdpass_option_max_word_length": "Longitud máxima de palabra",
|
||||||
|
"xkcdpass_option_max_word_length_description": "0 = sin límite, >0 = máximo de caracteres por palabra",
|
||||||
|
"xkcdpass_option_numberPosition": "Number Position",
|
||||||
|
"xkcdpass_option_numberPosition_list_beginning": "Al principio",
|
||||||
|
"xkcdpass_option_numberPosition_list_end": "Al final",
|
||||||
|
"xkcdpass_option_numberPosition_list_random": "Posición aleatoria",
|
||||||
|
"xkcdpass_option_separator": "Separador",
|
||||||
|
"xkcdpass_option_separator_description": "Carácter único o conjunto de caracteres (selección aleatoria). Los espacios se excluyen en conjuntos de varios caracteres.",
|
||||||
|
"xkcdpass_option_space_handling": "Manejo de espacios",
|
||||||
|
"xkcdpass_option_space_handling_description": "Cómo manejar las palabras que contienen espacios",
|
||||||
|
"xkcdpass_option_space_handling_list_allow": "Permitir (mantener espacios)",
|
||||||
|
"xkcdpass_option_space_handling_list_modify": "Modificar (reemplazar con _)",
|
||||||
|
"xkcdpass_option_space_handling_list_skip": "Omitir (no seleccionar)",
|
||||||
|
"xkcdpass_option_wordlist": "Lista de palabras",
|
||||||
|
"xkcdpass_option_words": "Palabras",
|
||||||
|
"xkcdpass_option_words_description": "Número de palabras (mínimo 2)",
|
||||||
|
"xkcdpass_title": "XKCD Pass",
|
||||||
|
"phraseabbreviator_number_zero": "cero",
|
||||||
|
"phraseabbreviator_number_one": "uno",
|
||||||
|
"phraseabbreviator_number_two": "dos",
|
||||||
|
"phraseabbreviator_number_three": "tres",
|
||||||
|
"phraseabbreviator_number_four": "cuatro",
|
||||||
|
"phraseabbreviator_number_five": "cinco",
|
||||||
|
"phraseabbreviator_number_six": "seis",
|
||||||
|
"phraseabbreviator_number_seven": "siete",
|
||||||
|
"phraseabbreviator_number_eight": "ocho",
|
||||||
|
"phraseabbreviator_number_nine": "nueve",
|
||||||
|
"phraseabbreviator_number_ten": "diez",
|
||||||
|
"phraseabbreviator_number_eleven": "once",
|
||||||
|
"phraseabbreviator_number_twelve": "doce",
|
||||||
|
"phraseabbreviator_number_thirteen": "trece",
|
||||||
|
"phraseabbreviator_number_fourteen": "catorce",
|
||||||
|
"phraseabbreviator_number_fifteen": "quince",
|
||||||
|
"phraseabbreviator_number_sixteen": "dieciséis",
|
||||||
|
"phraseabbreviator_number_seventeen": "diecisiete",
|
||||||
|
"phraseabbreviator_number_eighteen": "dieciocho",
|
||||||
|
"phraseabbreviator_number_nineteen": "diecinueve",
|
||||||
|
"phraseabbreviator_number_twenty": "veinte",
|
||||||
|
"xkcdpass_option_use_ai_wordlist": "Usar lista de palabras con IA",
|
||||||
|
"xkcdpass_option_use_ai_wordlist_description": "Genera una lista de palabras personalizada con un modelo de IA",
|
||||||
|
"xkcdpass_option_ai_wordlist_prompt": "Indicador de lista de palabras de IA",
|
||||||
|
"xkcdpass_option_ai_wordlist_prompt_description": "Completa la instrucción: \"Crea una lista de X palabras de al menos 5 caracteres, que...\"",
|
||||||
|
"xkcdpass_option_ai_provider": "Proveedor de IA",
|
||||||
|
"xkcdpass_option_ai_provider_description": "Qué proveedor LLM usar",
|
||||||
|
"phrase-abbreviator_option_use_ai_sentence": "Usar frase de IA",
|
||||||
|
"phrase-abbreviator_option_use_ai_sentence_description": "Permite que la IA genere una frase a partir de tu instrucción",
|
||||||
|
"phrase-abbreviator_option_ai_prompt": "Indicador de IA",
|
||||||
|
"phrase-abbreviator_option_ai_prompt_description": "Instrucción para que la IA genere una frase memorable (mínimo 12 palabras)",
|
||||||
|
"phrase-abbreviator_option_ai_provider": "Proveedor de IA",
|
||||||
|
"phrase-abbreviator_option_ai_provider_description": "Qué proveedor LLM usar para generar la frase",
|
||||||
|
"phrase-abbreviator_option_ai_model": "Modelo de IA",
|
||||||
|
"phrase-abbreviator_option_ai_model_description": "Qué modelo de IA usar",
|
||||||
|
"high-entropy_option_length_description": "Total length of the generated password",
|
||||||
|
"high-entropy_option_include_uppercase_description": "Include uppercase letters (A-Z)",
|
||||||
|
"high-entropy_option_include_lowercase_description": "Include lowercase letters (a-z)",
|
||||||
|
"high-entropy_option_include_numbers_description": "Include numbers (0-9)",
|
||||||
|
"high-entropy_option_include_symbols_description": "Include symbols (e.g., !@#$%)",
|
||||||
|
"xkcdpass_option_wordlist_description": "Wordlist to use for generating the passphrase",
|
||||||
|
"xkcdpass_option_wordlist_choice_en": "English",
|
||||||
|
"xkcdpass_option_wordlist_choice_de": "Deutsch",
|
||||||
|
"xkcdpass_option_wordlist_choice_fr": "Français",
|
||||||
|
"xkcdpass_option_wordlist_choice_it": "Italiano",
|
||||||
|
"xkcdpass_option_wordlist_choice_es": "Español",
|
||||||
|
"xkcdpass_option_space_handling_choice_allow": "Permitir",
|
||||||
|
"xkcdpass_option_space_handling_choice_skip": "Omitir",
|
||||||
|
"xkcdpass_option_space_handling_choice_modify": "Modificar",
|
||||||
|
"xkcdpass_option_add_number": "Agregar número",
|
||||||
|
"xkcdpass_option_add_number_description": "Add random number to the passphrase",
|
||||||
|
"xkcdpass_option_number_position": "Posición del número",
|
||||||
|
"xkcdpass_option_number_position_description": "Where to place the random number",
|
||||||
|
"xkcdpass_option_number_position_choice_beginning": "Al principio",
|
||||||
|
"xkcdpass_option_number_position_choice_end": "Al final",
|
||||||
|
"xkcdpass_option_number_position_choice_random": "Posición aleatoria",
|
||||||
|
"xkcdpass_option_ai_provider_choice_openai": "OpenAI",
|
||||||
|
"xkcdpass_option_ai_provider_choice_claude": "Claude",
|
||||||
|
"xkcdpass_option_ai_provider_choice_gemini": "Gemini",
|
||||||
|
"xkcdpass_option_ai_provider_choice_perplexity": "Perplexity",
|
||||||
|
"pwgen_option_type": "Preajuste de longitud",
|
||||||
|
"pwgen_option_type_description": "Preajuste heredado de longitud opcional. Si se establece en 8/12/16 caracteres, sobrescribe el valor de Longitud de arriba.",
|
||||||
|
"pwgen_option_type_choice_8-char": "8 caracteres",
|
||||||
|
"pwgen_option_type_choice_12-char": "12 caracteres",
|
||||||
|
"pwgen_option_type_choice_16-char": "16 caracteres",
|
||||||
|
"passkey_option_syllables_description": "Number of pronounceable syllables in the passkey",
|
||||||
|
"phrase-abbreviator_option_sentence": "Frase",
|
||||||
|
"phrase-abbreviator_option_sentence_description": "User-supplied sentence (minimum 8 words, must start with capital letter)",
|
||||||
|
"phrase-abbreviator_option_character_extraction": "Extracción de caracteres",
|
||||||
|
"phrase-abbreviator_option_character_extraction_description": "Method for extracting characters from each word",
|
||||||
|
"phrase-abbreviator_option_character_extraction_choice_single": "Solo la primera letra",
|
||||||
|
"phrase-abbreviator_option_character_extraction_choice_dual": "Primera letra + siguiente vocal/consonante",
|
||||||
|
"phrase-abbreviator_option_language": "Idioma",
|
||||||
|
"phrase-abbreviator_option_language_description": "Language for number word conversion",
|
||||||
|
"phrase-abbreviator_option_language_choice_en": "English",
|
||||||
|
"phrase-abbreviator_option_language_choice_de": "Alemán",
|
||||||
|
"phrase-abbreviator_option_language_choice_de_CH": "Suizo alemán",
|
||||||
|
"phrase-abbreviator_option_language_choice_fr": "Francés",
|
||||||
|
"phrase-abbreviator_option_language_choice_it": "Italiano",
|
||||||
|
"phrase-abbreviator_option_language_choice_es": "Español",
|
||||||
|
"phrase-abbreviator_option_language_choice_pt": "Portugués",
|
||||||
|
"phrase-abbreviator_option_language_choice_ja": "Japonés",
|
||||||
|
"phrase-abbreviator_option_language_choice_zh": "Chino",
|
||||||
|
"phrase-abbreviator_option_add_number": "Agregar número",
|
||||||
|
"phrase-abbreviator_option_add_number_description": "Agregar dígitos aleatorios a la contraseña",
|
||||||
|
"phrase-abbreviator_option_number_digits": "Cantidad de dígitos",
|
||||||
|
"phrase-abbreviator_option_number_digits_description": "Cuántos dígitos aleatorios agregar (1-4)",
|
||||||
|
"phrase-abbreviator_option_number_position": "Posición del número",
|
||||||
|
"phrase-abbreviator_option_number_position_description": "Colocar el número antes o después de la contraseña",
|
||||||
|
"phrase-abbreviator_option_number_position_choice_prepend": "Antes",
|
||||||
|
"phrase-abbreviator_option_number_position_choice_append": "Después",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_openai": "OpenAI",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_claude": "Claude",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_gemini": "Gemini",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_perplexity": "Perplexity",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-3.5-turbo": "GPT-3.5 Turbo",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4": "GPT-4",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4-turbo": "GPT-4 Turbo",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4o": "GPT-4 Omni",
|
||||||
|
"collapse_group_basic": "Opciones básicas",
|
||||||
|
"collapse_group_advanced": "Opciones avanzadas",
|
||||||
|
"collapse_group_ai": "Funciones de IA",
|
||||||
|
"collapse_group_expert": "Opciones expertas",
|
||||||
|
"collapse_group_character_set": "Conjunto de caracteres",
|
||||||
|
"copied": "Copiado",
|
||||||
|
"pwgen_option_type_choice_custom": "Usar ajuste de longitud",
|
||||||
|
"roulette": "Ruleta",
|
||||||
|
"stop": "Detener",
|
||||||
|
"roulette_interval": "Intervalo (s)",
|
||||||
|
"roulette_slow_down": "Aumentar intervalo (más lento)",
|
||||||
|
"roulette_speed_up": "Reducir intervalo (más rápido)"
|
||||||
|
}
|
||||||
+223
@@ -0,0 +1,223 @@
|
|||||||
|
{
|
||||||
|
"api_documentation": "Documentation API",
|
||||||
|
"app_title": "app_title",
|
||||||
|
"close": "Fermer",
|
||||||
|
"copied_to_clipboard": "Copié dans le presse-papiers !",
|
||||||
|
"copy": "Copier",
|
||||||
|
"copy_failed": "Échec de la copie",
|
||||||
|
"count": "Nombre",
|
||||||
|
"deutsch": "Deutsch",
|
||||||
|
"dice_rolls": "Lancers de dés",
|
||||||
|
"diceware_badge": "Priorité à la sécurité",
|
||||||
|
"diceware_description": "Méthode EFF Diceware authentique utilisant des lancers de dés cryptographiquement sécurisés pour une sécurité maximale",
|
||||||
|
"diceware_option_show_dice_rolls": "Show Dice Rolls",
|
||||||
|
"diceware_option_show_dice_rolls_description": "Pédagogique : affiche les lancers de dés utilisés pour choisir chaque mot",
|
||||||
|
"diceware_option_uppercase_first": "Capitalize First Word",
|
||||||
|
"diceware_option_uppercase_first_description": "Met uniquement la première lettre en majuscule (style Diceware traditionnel)",
|
||||||
|
"diceware_option_words": "Words",
|
||||||
|
"diceware_option_words_description": "Au moins 6 mots recommandés pour une sécurité optimale (77,5 bits d’entropie).",
|
||||||
|
"diceware_title": "Diceware",
|
||||||
|
"english": "Anglais",
|
||||||
|
"entropy": "Entropie",
|
||||||
|
"length": "Longueur",
|
||||||
|
"details": "Détails",
|
||||||
|
"security_level": "Niveau de sécurité",
|
||||||
|
"very_strong": "Très fort",
|
||||||
|
"strong": "Fort",
|
||||||
|
"adequate": "Adéquat",
|
||||||
|
"weak": "Faible",
|
||||||
|
"very_weak": "Très faible",
|
||||||
|
"chars": "caractères",
|
||||||
|
"error_generating_passwords": "Erreur lors de la génération des mots de passe",
|
||||||
|
"espanol": "Español",
|
||||||
|
"footer_text": "footer_text",
|
||||||
|
"francais": "Français",
|
||||||
|
"generate": "Générer",
|
||||||
|
"generating": "Génération...",
|
||||||
|
"high-entropy_description": "Mots de passe aléatoires cryptographiquement sécurisés avec entropie maximale",
|
||||||
|
"high-entropy_option_include_lowercase": "Include Lowercase",
|
||||||
|
"high-entropy_option_include_numbers": "Include Numbers",
|
||||||
|
"high-entropy_option_include_symbols": "Include Symbols",
|
||||||
|
"high-entropy_option_include_uppercase": "Include Uppercase",
|
||||||
|
"high-entropy_option_length": "Length",
|
||||||
|
"high-entropy_title": "High Entropy",
|
||||||
|
"info_spaces_replaced_with_underscores": "Les espaces dans les mots sélectionnés sont remplacés par des traits de soulignement (_).",
|
||||||
|
"info_words_with_spaces_excluded": "Les mots contenant des espaces sont exclus de la sélection.",
|
||||||
|
"italiano": "Italiano",
|
||||||
|
"language": "Langue :",
|
||||||
|
"loading_generators": "loading_generators",
|
||||||
|
"loading_password_generators": "Chargement des générateurs de mots de passe...",
|
||||||
|
"native_group_explanation": "Options natives du générateur PwGen pour créer des mots de passe prononçables.",
|
||||||
|
"native_pwgen": "Options natives",
|
||||||
|
"no_spaces_in_separator": "N’utilisez pas d’espaces dans le séparateur lorsque le mode de traitement des espaces est défini sur « modifier » ou « ignorer ».",
|
||||||
|
"passkey_description": "Mots de passe prononçables de style Apple avec substitutions de caractères intégrées",
|
||||||
|
"passkey_option_syllables": "Syllables",
|
||||||
|
"passkey_title": "Passkey",
|
||||||
|
"password_generator": "Générateur de mots de passe",
|
||||||
|
"php_fallback": "Code de secours en PHP lorsque pwgen natif n’est pas disponible.",
|
||||||
|
"portugues": "Portugais",
|
||||||
|
"pwgen_badge": "Amélioré",
|
||||||
|
"pwgen_description": "Mots de passe de style pwgen classique avec motifs lisibles",
|
||||||
|
"pwgen_option_include_capitals": "Inclure les majuscules",
|
||||||
|
"pwgen_option_include_capitals_description": "Les mots de passe générés contiendront des lettres majuscules.",
|
||||||
|
"pwgen_option_include_numbers": "Inclure les chiffres",
|
||||||
|
"pwgen_option_include_numbers_description": "Les mots de passe générés contiendront des chiffres.",
|
||||||
|
"pwgen_option_length": "Longueur",
|
||||||
|
"pwgen_option_length_description": "Longueur totale du mot de passe, y compris les séparateurs (minimum 4 caractères)",
|
||||||
|
"pwgen_option_no_ambiguous": "Sans caractères ambiguës",
|
||||||
|
"pwgen_option_no_ambiguous_description": "Exclure des caractères comme 0, O, l, 1, etc. pour améliorer la lisibilité",
|
||||||
|
"pwgen_option_pattern_complexity": "Motif complexe",
|
||||||
|
"pwgen_option_pattern_complexity_description": "Contrôle la complexité des motifs générés.",
|
||||||
|
"pwgen_option_pattern_complexity_list_balanced": "Complexité équilibrée",
|
||||||
|
"pwgen_option_pattern_complexity_list_complex": "Motifs complexes",
|
||||||
|
"pwgen_option_pattern_complexity_list_simple": "Motifs simples",
|
||||||
|
"pwgen_option_secure_mode": "Mode sécurisé",
|
||||||
|
"pwgen_option_secure_mode_description": "Génère des mots de passe entièrement aléatoires au lieu de mots prononçables",
|
||||||
|
"pwgen_option_separator": "Séparateur",
|
||||||
|
"pwgen_option_separator_description": "Caractère(s) pour séparer les syllabes. Si plusieurs caractères sont définis, ils sont séparés par un caractère choisi au hasard. Si ce champ est vide, aucune séparation n’est effectuée.",
|
||||||
|
"pwgen_option_syllable_count": "Nombre de syllabes",
|
||||||
|
"pwgen_option_syllable_count_description": "Nombre de syllabes à générer (n’utilisé qu’avec un séparateur)",
|
||||||
|
"pwgen_title": "PWGen",
|
||||||
|
"results": "Résultats",
|
||||||
|
"schwyzerduetsch": "Schwyzerdütsch",
|
||||||
|
"secure_mode_disables_syllable_separation": "Le mode sécurisé désactive la séparation des syllabes.",
|
||||||
|
"separator_method_info": "Informations sur la méthode de séparation",
|
||||||
|
"single_space_allowed": "Un seul espace autorisé",
|
||||||
|
"success": "Succès",
|
||||||
|
"syllable_group_explanation": "Explication des syllabes",
|
||||||
|
"syllable_length_too_short": "La syllabe est trop courte",
|
||||||
|
"syllable_length_very_long": "La syllabe est très longue",
|
||||||
|
"warning_short_word_limits_reduces_choices": "Avertissement : les limites de mots courts réduisent les choix",
|
||||||
|
"warning_short_words_reduce_memorability": "Avertissement : les mots courts réduisent la mémorisation",
|
||||||
|
"xkcdpass_badge": "Priorité à la mémorisation",
|
||||||
|
"xkcdpass_description": "Description xkcdpass",
|
||||||
|
"xkcdpass_option_addNumber": "Add Number",
|
||||||
|
"xkcdpass_option_max_word_length": "Longueur maximale du mot",
|
||||||
|
"xkcdpass_option_max_word_length_description": "0 = pas de limite, >0 = nombre maximal de caractères par mot",
|
||||||
|
"xkcdpass_option_numberPosition": "Number Position",
|
||||||
|
"xkcdpass_option_numberPosition_list_beginning": "Au début",
|
||||||
|
"xkcdpass_option_numberPosition_list_end": "À la fin",
|
||||||
|
"xkcdpass_option_numberPosition_list_random": "Position aléatoire",
|
||||||
|
"xkcdpass_option_separator": "Séparateur",
|
||||||
|
"xkcdpass_option_separator_description": "Caractère unique ou ensemble de caractères (sélection aléatoire). Les espaces sont exclus des ensembles multi-caractères.",
|
||||||
|
"xkcdpass_option_space_handling": "Gestion des espaces",
|
||||||
|
"xkcdpass_option_space_handling_description": "Comment gérer les mots contenant des espaces",
|
||||||
|
"xkcdpass_option_space_handling_list_allow": "Autoriser (conserver les espaces)",
|
||||||
|
"xkcdpass_option_space_handling_list_modify": "Modifier (remplacer par _)",
|
||||||
|
"xkcdpass_option_space_handling_list_skip": "Ignorer (ne pas sélectionner)",
|
||||||
|
"xkcdpass_option_wordlist": "Liste de mots",
|
||||||
|
"xkcdpass_option_words": "Mots",
|
||||||
|
"xkcdpass_option_words_description": "Nombre de mots (minimum 2)",
|
||||||
|
"xkcdpass_title": "XKCD Pass",
|
||||||
|
"phraseabbreviator_number_zero": "zéro",
|
||||||
|
"phraseabbreviator_number_one": "un",
|
||||||
|
"phraseabbreviator_number_two": "deux",
|
||||||
|
"phraseabbreviator_number_three": "trois",
|
||||||
|
"phraseabbreviator_number_four": "quatre",
|
||||||
|
"phraseabbreviator_number_five": "cinq",
|
||||||
|
"phraseabbreviator_number_six": "six",
|
||||||
|
"phraseabbreviator_number_seven": "sept",
|
||||||
|
"phraseabbreviator_number_eight": "huit",
|
||||||
|
"phraseabbreviator_number_nine": "neuf",
|
||||||
|
"phraseabbreviator_number_ten": "dix",
|
||||||
|
"phraseabbreviator_number_eleven": "onze",
|
||||||
|
"phraseabbreviator_number_twelve": "douze",
|
||||||
|
"phraseabbreviator_number_thirteen": "treize",
|
||||||
|
"phraseabbreviator_number_fourteen": "quatorze",
|
||||||
|
"phraseabbreviator_number_fifteen": "quinze",
|
||||||
|
"phraseabbreviator_number_sixteen": "seize",
|
||||||
|
"phraseabbreviator_number_seventeen": "dix-sept",
|
||||||
|
"phraseabbreviator_number_eighteen": "dix-huit",
|
||||||
|
"phraseabbreviator_number_nineteen": "dix-neuf",
|
||||||
|
"phraseabbreviator_number_twenty": "vingt",
|
||||||
|
"xkcdpass_option_use_ai_wordlist": "Utiliser la liste de mots IA",
|
||||||
|
"xkcdpass_option_use_ai_wordlist_description": "Génère une liste de mots personnalisée avec un modèle d’IA",
|
||||||
|
"xkcdpass_option_ai_wordlist_prompt": "Invite de liste de mots IA",
|
||||||
|
"xkcdpass_option_ai_wordlist_prompt_description": "Complétez l’invite : \"Crée une liste de X mots d’au moins 5 caractères, qui...\"",
|
||||||
|
"xkcdpass_option_ai_provider": "Fournisseur IA",
|
||||||
|
"xkcdpass_option_ai_provider_description": "Quel fournisseur LLM utiliser",
|
||||||
|
"phrase-abbreviator_option_use_ai_sentence": "Utiliser une phrase IA",
|
||||||
|
"phrase-abbreviator_option_use_ai_sentence_description": "Permet à l’IA de générer une phrase à partir de votre invite",
|
||||||
|
"phrase-abbreviator_option_ai_prompt": "Invite IA",
|
||||||
|
"phrase-abbreviator_option_ai_prompt_description": "Invite pour que l’IA génère une phrase mémorable (minimum 12 mots)",
|
||||||
|
"phrase-abbreviator_option_ai_provider": "Fournisseur IA",
|
||||||
|
"phrase-abbreviator_option_ai_provider_description": "Quel fournisseur LLM utiliser pour la génération de phrases",
|
||||||
|
"phrase-abbreviator_option_ai_model": "Modèle IA",
|
||||||
|
"phrase-abbreviator_option_ai_model_description": "Quel modèle IA utiliser",
|
||||||
|
"high-entropy_option_length_description": "Total length of the generated password",
|
||||||
|
"high-entropy_option_include_uppercase_description": "Include uppercase letters (A-Z)",
|
||||||
|
"high-entropy_option_include_lowercase_description": "Include lowercase letters (a-z)",
|
||||||
|
"high-entropy_option_include_numbers_description": "Include numbers (0-9)",
|
||||||
|
"high-entropy_option_include_symbols_description": "Include symbols (e.g., !@#$%)",
|
||||||
|
"xkcdpass_option_wordlist_description": "Wordlist to use for generating the passphrase",
|
||||||
|
"xkcdpass_option_wordlist_choice_en": "English",
|
||||||
|
"xkcdpass_option_wordlist_choice_de": "Deutsch",
|
||||||
|
"xkcdpass_option_wordlist_choice_fr": "Français",
|
||||||
|
"xkcdpass_option_wordlist_choice_it": "Italiano",
|
||||||
|
"xkcdpass_option_wordlist_choice_es": "Español",
|
||||||
|
"xkcdpass_option_space_handling_choice_allow": "Autoriser",
|
||||||
|
"xkcdpass_option_space_handling_choice_skip": "Ignorer",
|
||||||
|
"xkcdpass_option_space_handling_choice_modify": "Modifier",
|
||||||
|
"xkcdpass_option_add_number": "Ajouter un nombre",
|
||||||
|
"xkcdpass_option_add_number_description": "Add random number to the passphrase",
|
||||||
|
"xkcdpass_option_number_position": "Position du nombre",
|
||||||
|
"xkcdpass_option_number_position_description": "Where to place the random number",
|
||||||
|
"xkcdpass_option_number_position_choice_beginning": "Au début",
|
||||||
|
"xkcdpass_option_number_position_choice_end": "À la fin",
|
||||||
|
"xkcdpass_option_number_position_choice_random": "Position aléatoire",
|
||||||
|
"xkcdpass_option_ai_provider_choice_openai": "OpenAI",
|
||||||
|
"xkcdpass_option_ai_provider_choice_claude": "Claude",
|
||||||
|
"xkcdpass_option_ai_provider_choice_gemini": "Gemini",
|
||||||
|
"xkcdpass_option_ai_provider_choice_perplexity": "Perplexity",
|
||||||
|
"pwgen_option_type": "Préréglage de longueur",
|
||||||
|
"pwgen_option_type_description": "Préréglage de longueur facultatif. S'il est défini sur 8/12/16 caractères, il remplace la valeur Longueur ci-dessus.",
|
||||||
|
"pwgen_option_type_choice_8-char": "8 caractères",
|
||||||
|
"pwgen_option_type_choice_12-char": "12 caractères",
|
||||||
|
"pwgen_option_type_choice_16-char": "16 caractères",
|
||||||
|
"passkey_option_syllables_description": "Number of pronounceable syllables in the passkey",
|
||||||
|
"phrase-abbreviator_option_sentence": "Phrase",
|
||||||
|
"phrase-abbreviator_option_sentence_description": "User-supplied sentence (minimum 8 words, must start with capital letter)",
|
||||||
|
"phrase-abbreviator_option_character_extraction": "Extraction des caractères",
|
||||||
|
"phrase-abbreviator_option_character_extraction_description": "Method for extracting characters from each word",
|
||||||
|
"phrase-abbreviator_option_character_extraction_choice_single": "Première lettre uniquement",
|
||||||
|
"phrase-abbreviator_option_character_extraction_choice_dual": "Première lettre + prochaine voyelle/consonne",
|
||||||
|
"phrase-abbreviator_option_language": "Langue",
|
||||||
|
"phrase-abbreviator_option_language_description": "Language for number word conversion",
|
||||||
|
"phrase-abbreviator_option_language_choice_en": "English",
|
||||||
|
"phrase-abbreviator_option_language_choice_de": "Allemand",
|
||||||
|
"phrase-abbreviator_option_language_choice_de_CH": "Suisse allemand",
|
||||||
|
"phrase-abbreviator_option_language_choice_fr": "Français",
|
||||||
|
"phrase-abbreviator_option_language_choice_it": "Italien",
|
||||||
|
"phrase-abbreviator_option_language_choice_es": "Espagnol",
|
||||||
|
"phrase-abbreviator_option_language_choice_pt": "Portugais",
|
||||||
|
"phrase-abbreviator_option_language_choice_ja": "Japonais",
|
||||||
|
"phrase-abbreviator_option_language_choice_zh": "Chinois",
|
||||||
|
"phrase-abbreviator_option_add_number": "Ajouter un nombre",
|
||||||
|
"phrase-abbreviator_option_add_number_description": "Ajouter des chiffres aléatoires au mot de passe",
|
||||||
|
"phrase-abbreviator_option_number_digits": "Nombre de chiffres",
|
||||||
|
"phrase-abbreviator_option_number_digits_description": "Combien de chiffres aléatoires ajouter (1-4)",
|
||||||
|
"phrase-abbreviator_option_number_position": "Position du nombre",
|
||||||
|
"phrase-abbreviator_option_number_position_description": "Place le nombre avant ou après le mot de passe",
|
||||||
|
"phrase-abbreviator_option_number_position_choice_prepend": "Avant",
|
||||||
|
"phrase-abbreviator_option_number_position_choice_append": "Après",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_openai": "OpenAI",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_claude": "Claude",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_gemini": "Gemini",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_perplexity": "Perplexity",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-3.5-turbo": "GPT-3.5 Turbo",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4": "GPT-4",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4-turbo": "GPT-4 Turbo",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4o": "GPT-4 Omni",
|
||||||
|
"collapse_group_basic": "Options de base",
|
||||||
|
"collapse_group_advanced": "Options avancées",
|
||||||
|
"collapse_group_ai": "Fonctionnalités IA",
|
||||||
|
"collapse_group_expert": "Options expertes",
|
||||||
|
"collapse_group_character_set": "Jeu de caractères",
|
||||||
|
"copied": "Copié",
|
||||||
|
"pwgen_option_type_choice_custom": "Utiliser le réglage de longueur",
|
||||||
|
"roulette": "Roulette",
|
||||||
|
"stop": "Arrêter",
|
||||||
|
"roulette_interval": "Intervalle (s)",
|
||||||
|
"roulette_slow_down": "Augmenter l'intervalle (plus lent)",
|
||||||
|
"roulette_speed_up": "Réduire l'intervalle (plus rapide)"
|
||||||
|
}
|
||||||
+223
@@ -0,0 +1,223 @@
|
|||||||
|
{
|
||||||
|
"api_documentation": "Documentazione API",
|
||||||
|
"app_title": "Generatore di Password",
|
||||||
|
"close": "Chiudi",
|
||||||
|
"copied_to_clipboard": "Copiato negli appunti!",
|
||||||
|
"copy": "Copia",
|
||||||
|
"copy_failed": "Copia fallita",
|
||||||
|
"count": "Conteggio",
|
||||||
|
"deutsch": "Deutsch",
|
||||||
|
"dice_rolls": "Lanci di dadi",
|
||||||
|
"diceware_badge": "Focus Sicurezza",
|
||||||
|
"diceware_description": "Metodo EFF Diceware autentico utilizzando lanci di dadi crittograficamente sicuri per la massima sicurezza",
|
||||||
|
"diceware_option_show_dice_rolls": "Show Dice Rolls",
|
||||||
|
"diceware_option_show_dice_rolls_description": "Educational: display the dice rolls used to select each word",
|
||||||
|
"diceware_option_uppercase_first": "Capitalize First Word",
|
||||||
|
"diceware_option_uppercase_first_description": "Capitalize only the first word (traditional Diceware style)",
|
||||||
|
"diceware_option_words": "Words",
|
||||||
|
"diceware_option_words_description": "Minimum 6 words recommended for security (77.5 bits entropy)",
|
||||||
|
"diceware_title": "Diceware",
|
||||||
|
"english": "Inglese",
|
||||||
|
"entropy": "Entropia",
|
||||||
|
"length": "Lunghezza",
|
||||||
|
"details": "Dettagli",
|
||||||
|
"security_level": "Livello di sicurezza",
|
||||||
|
"very_strong": "Molto forte",
|
||||||
|
"strong": "Forte",
|
||||||
|
"adequate": "Adeguata",
|
||||||
|
"weak": "Debole",
|
||||||
|
"very_weak": "Molto debole",
|
||||||
|
"chars": "caratteri",
|
||||||
|
"error_generating_passwords": "Errore nella generazione delle password",
|
||||||
|
"espanol": "Español",
|
||||||
|
"footer_text": "testo del piè di pagina",
|
||||||
|
"francais": "Français",
|
||||||
|
"generate": "Genera",
|
||||||
|
"generating": "Generazione...",
|
||||||
|
"high-entropy_description": "Password casuali crittograficamente sicure con entropia massima",
|
||||||
|
"high-entropy_option_include_lowercase": "Include Lowercase",
|
||||||
|
"high-entropy_option_include_numbers": "Include Numbers",
|
||||||
|
"high-entropy_option_include_symbols": "Include Symbols",
|
||||||
|
"high-entropy_option_include_uppercase": "Include Uppercase",
|
||||||
|
"high-entropy_option_length": "Length",
|
||||||
|
"high-entropy_title": "High Entropy",
|
||||||
|
"info_spaces_replaced_with_underscores": "Gli spazi nelle parole selezionate vengono sostituiti con underscore (_).",
|
||||||
|
"info_words_with_spaces_excluded": "Le parole che contengono spazi vengono escluse dalla selezione.",
|
||||||
|
"italiano": "Italiano",
|
||||||
|
"language": "Lingua:",
|
||||||
|
"loading_generators": "Caricamento generatori",
|
||||||
|
"loading_password_generators": "Caricamento generatori di password...",
|
||||||
|
"native_group_explanation": "Opzioni native del generatore PwGen per creare password pronunciabili.",
|
||||||
|
"native_pwgen": "Opzioni native",
|
||||||
|
"no_spaces_in_separator": "Non usare spazi nel separatore quando la gestione degli spazi è impostata su “modifica” o “salta”.",
|
||||||
|
"passkey_description": "Password pronunciabili in stile Apple con sostituzioni di caratteri integrate",
|
||||||
|
"passkey_option_syllables": "Syllables",
|
||||||
|
"passkey_title": "Passkey",
|
||||||
|
"password_generator": "Generatore password",
|
||||||
|
"php_fallback": "Codice di fallback in PHP quando pwgen nativo non è disponibile.",
|
||||||
|
"portugues": "Portoghese",
|
||||||
|
"pwgen_badge": "Migliorato",
|
||||||
|
"pwgen_description": "Password in stile pwgen classico con pattern leggibili",
|
||||||
|
"pwgen_option_include_capitals": "Includi maiuscole",
|
||||||
|
"pwgen_option_include_capitals_description": "Le password generate conterranno lettere maiuscole.",
|
||||||
|
"pwgen_option_include_numbers": "Includi numeri",
|
||||||
|
"pwgen_option_include_numbers_description": "Le password generate conterranno cifre.",
|
||||||
|
"pwgen_option_length": "Lunghezza",
|
||||||
|
"pwgen_option_length_description": "Lunghezza totale della password inclusi i separatori (minimo 4 caratteri)",
|
||||||
|
"pwgen_option_no_ambiguous": "Nessun carattere ambiguo",
|
||||||
|
"pwgen_option_no_ambiguous_description": "Escludi caratteri come 0, O, l, 1, ecc. per migliorare la leggibilità",
|
||||||
|
"pwgen_option_pattern_complexity": "Pattern complesso",
|
||||||
|
"pwgen_option_pattern_complexity_description": "Controlla la complessità dei pattern generati.",
|
||||||
|
"pwgen_option_pattern_complexity_list_balanced": "Complessità bilanciata",
|
||||||
|
"pwgen_option_pattern_complexity_list_complex": "Pattern complessi",
|
||||||
|
"pwgen_option_pattern_complexity_list_simple": "Pattern semplici",
|
||||||
|
"pwgen_option_secure_mode": "Modalità sicura",
|
||||||
|
"pwgen_option_secure_mode_description": "Genera password completamente casuali invece di parole pronunciabili",
|
||||||
|
"pwgen_option_separator": "Separatore",
|
||||||
|
"pwgen_option_separator_description": "Carattere(i) per separare le sillabe. Se ne vengono definiti più di uno, vengono separati da un carattere scelto casualmente. Se è vuoto, non viene eseguita alcuna separazione.",
|
||||||
|
"pwgen_option_syllable_count": "Numero di sillabe",
|
||||||
|
"pwgen_option_syllable_count_description": "Numero di sillabe da generare (usato solo con separatore)",
|
||||||
|
"pwgen_title": "PWGen",
|
||||||
|
"results": "Risultati",
|
||||||
|
"schwyzerduetsch": "Tedesco Svizzero",
|
||||||
|
"secure_mode_disables_syllable_separation": "La modalità sicura disattiva la separazione delle sillabe.",
|
||||||
|
"separator_method_info": "Informazioni sul metodo di separazione",
|
||||||
|
"single_space_allowed": "È consentito un solo spazio",
|
||||||
|
"success": "Successo",
|
||||||
|
"syllable_group_explanation": "Spiegazione delle sillabe",
|
||||||
|
"syllable_length_too_short": "La sillaba è troppo breve",
|
||||||
|
"syllable_length_very_long": "La sillaba è molto lunga",
|
||||||
|
"warning_short_word_limits_reduces_choices": "Avviso: i limiti di parole brevi riducono le scelte",
|
||||||
|
"warning_short_words_reduce_memorability": "Avviso: le parole brevi riducono la memorizzazione",
|
||||||
|
"xkcdpass_badge": "Focus Memorabilità",
|
||||||
|
"xkcdpass_description": "Descrizione xkcdpass",
|
||||||
|
"xkcdpass_option_addNumber": "Add Number",
|
||||||
|
"xkcdpass_option_max_word_length": "Lunghezza massima parola",
|
||||||
|
"xkcdpass_option_max_word_length_description": "0 = no limit, >0 = maximum characters per word",
|
||||||
|
"xkcdpass_option_numberPosition": "Number Position",
|
||||||
|
"xkcdpass_option_numberPosition_list_beginning": "All’inizio",
|
||||||
|
"xkcdpass_option_numberPosition_list_end": "Alla fine",
|
||||||
|
"xkcdpass_option_numberPosition_list_random": "Posizione casuale",
|
||||||
|
"xkcdpass_option_separator": "Separatore",
|
||||||
|
"xkcdpass_option_separator_description": "Single character or character set (random selection). Spaces excluded from multi-character sets.",
|
||||||
|
"xkcdpass_option_space_handling": "Gestione spazi",
|
||||||
|
"xkcdpass_option_space_handling_description": "Come gestire le parole contenenti spazi",
|
||||||
|
"xkcdpass_option_space_handling_list_allow": "Consenti (mantieni spazi)",
|
||||||
|
"xkcdpass_option_space_handling_list_modify": "Modifica (sostituisci con _)",
|
||||||
|
"xkcdpass_option_space_handling_list_skip": "Salta (non selezionare)",
|
||||||
|
"xkcdpass_option_wordlist": "Lista di parole",
|
||||||
|
"xkcdpass_option_words": "Parole",
|
||||||
|
"xkcdpass_option_words_description": "Number of words (minimal 2)",
|
||||||
|
"xkcdpass_title": "XKCD Pass",
|
||||||
|
"phraseabbreviator_number_zero": "zero",
|
||||||
|
"phraseabbreviator_number_one": "uno",
|
||||||
|
"phraseabbreviator_number_two": "due",
|
||||||
|
"phraseabbreviator_number_three": "tre",
|
||||||
|
"phraseabbreviator_number_four": "quattro",
|
||||||
|
"phraseabbreviator_number_five": "cinque",
|
||||||
|
"phraseabbreviator_number_six": "sei",
|
||||||
|
"phraseabbreviator_number_seven": "sette",
|
||||||
|
"phraseabbreviator_number_eight": "otto",
|
||||||
|
"phraseabbreviator_number_nine": "nove",
|
||||||
|
"phraseabbreviator_number_ten": "dieci",
|
||||||
|
"phraseabbreviator_number_eleven": "undici",
|
||||||
|
"phraseabbreviator_number_twelve": "dodici",
|
||||||
|
"phraseabbreviator_number_thirteen": "tredici",
|
||||||
|
"phraseabbreviator_number_fourteen": "quattordici",
|
||||||
|
"phraseabbreviator_number_fifteen": "quindici",
|
||||||
|
"phraseabbreviator_number_sixteen": "sedici",
|
||||||
|
"phraseabbreviator_number_seventeen": "diciassette",
|
||||||
|
"phraseabbreviator_number_eighteen": "diciotto",
|
||||||
|
"phraseabbreviator_number_nineteen": "diciannove",
|
||||||
|
"phraseabbreviator_number_twenty": "venti",
|
||||||
|
"xkcdpass_option_use_ai_wordlist": "Usa lista parole IA",
|
||||||
|
"xkcdpass_option_use_ai_wordlist_description": "Genera una lista di parole personalizzata con un modello IA",
|
||||||
|
"xkcdpass_option_ai_wordlist_prompt": "Prompt lista parole IA",
|
||||||
|
"xkcdpass_option_ai_wordlist_prompt_description": "Completa il prompt: \"Crea una lista di X parole di almeno 5 caratteri, che...\"",
|
||||||
|
"xkcdpass_option_ai_provider": "Provider IA",
|
||||||
|
"xkcdpass_option_ai_provider_description": "Quale provider LLM usare",
|
||||||
|
"phrase-abbreviator_option_use_ai_sentence": "Usa frase IA",
|
||||||
|
"phrase-abbreviator_option_use_ai_sentence_description": "Consente all’IA di generare una frase dal tuo prompt",
|
||||||
|
"phrase-abbreviator_option_ai_prompt": "Prompt IA",
|
||||||
|
"phrase-abbreviator_option_ai_prompt_description": "Prompt per far generare all’IA una frase memorabile (minimo 12 parole)",
|
||||||
|
"phrase-abbreviator_option_ai_provider": "Provider IA",
|
||||||
|
"phrase-abbreviator_option_ai_provider_description": "Quale provider LLM usare per la generazione della frase",
|
||||||
|
"phrase-abbreviator_option_ai_model": "Modello IA",
|
||||||
|
"phrase-abbreviator_option_ai_model_description": "Quale modello IA usare",
|
||||||
|
"high-entropy_option_length_description": "Total length of the generated password",
|
||||||
|
"high-entropy_option_include_uppercase_description": "Include uppercase letters (A-Z)",
|
||||||
|
"high-entropy_option_include_lowercase_description": "Include lowercase letters (a-z)",
|
||||||
|
"high-entropy_option_include_numbers_description": "Include numbers (0-9)",
|
||||||
|
"high-entropy_option_include_symbols_description": "Include symbols (e.g., !@#$%)",
|
||||||
|
"xkcdpass_option_wordlist_description": "Wordlist to use for generating the passphrase",
|
||||||
|
"xkcdpass_option_wordlist_choice_en": "English",
|
||||||
|
"xkcdpass_option_wordlist_choice_de": "Deutsch",
|
||||||
|
"xkcdpass_option_wordlist_choice_fr": "Français",
|
||||||
|
"xkcdpass_option_wordlist_choice_it": "Italiano",
|
||||||
|
"xkcdpass_option_wordlist_choice_es": "Español",
|
||||||
|
"xkcdpass_option_space_handling_choice_allow": "Consenti",
|
||||||
|
"xkcdpass_option_space_handling_choice_skip": "Salta",
|
||||||
|
"xkcdpass_option_space_handling_choice_modify": "Modifica",
|
||||||
|
"xkcdpass_option_add_number": "Aggiungi numero",
|
||||||
|
"xkcdpass_option_add_number_description": "Add random number to the passphrase",
|
||||||
|
"xkcdpass_option_number_position": "Posizione del numero",
|
||||||
|
"xkcdpass_option_number_position_description": "Where to place the random number",
|
||||||
|
"xkcdpass_option_number_position_choice_beginning": "All’inizio",
|
||||||
|
"xkcdpass_option_number_position_choice_end": "Alla fine",
|
||||||
|
"xkcdpass_option_number_position_choice_random": "Posizione casuale",
|
||||||
|
"xkcdpass_option_ai_provider_choice_openai": "OpenAI",
|
||||||
|
"xkcdpass_option_ai_provider_choice_claude": "Claude",
|
||||||
|
"xkcdpass_option_ai_provider_choice_gemini": "Gemini",
|
||||||
|
"xkcdpass_option_ai_provider_choice_perplexity": "Perplexity",
|
||||||
|
"pwgen_option_type": "Preset lunghezza",
|
||||||
|
"pwgen_option_type_description": "Preset di lunghezza opzionale. Se impostato su 8/12/16 caratteri, sovrascrive il valore Lunghezza sopra.",
|
||||||
|
"pwgen_option_type_choice_8-char": "8 caratteri",
|
||||||
|
"pwgen_option_type_choice_12-char": "12 caratteri",
|
||||||
|
"pwgen_option_type_choice_16-char": "16 caratteri",
|
||||||
|
"passkey_option_syllables_description": "Number of pronounceable syllables in the passkey",
|
||||||
|
"phrase-abbreviator_option_sentence": "Frase",
|
||||||
|
"phrase-abbreviator_option_sentence_description": "User-supplied sentence (minimum 8 words, must start with capital letter)",
|
||||||
|
"phrase-abbreviator_option_character_extraction": "Estrazione caratteri",
|
||||||
|
"phrase-abbreviator_option_character_extraction_description": "Method for extracting characters from each word",
|
||||||
|
"phrase-abbreviator_option_character_extraction_choice_single": "Solo prima lettera",
|
||||||
|
"phrase-abbreviator_option_character_extraction_choice_dual": "Prima lettera + prossima vocale/consonante",
|
||||||
|
"phrase-abbreviator_option_language": "Lingua",
|
||||||
|
"phrase-abbreviator_option_language_description": "Language for number word conversion",
|
||||||
|
"phrase-abbreviator_option_language_choice_en": "English",
|
||||||
|
"phrase-abbreviator_option_language_choice_de": "Tedesco",
|
||||||
|
"phrase-abbreviator_option_language_choice_de_CH": "Svizzero tedesco",
|
||||||
|
"phrase-abbreviator_option_language_choice_fr": "Francese",
|
||||||
|
"phrase-abbreviator_option_language_choice_it": "Italiano",
|
||||||
|
"phrase-abbreviator_option_language_choice_es": "Spagnolo",
|
||||||
|
"phrase-abbreviator_option_language_choice_pt": "Portoghese",
|
||||||
|
"phrase-abbreviator_option_language_choice_ja": "Giapponese",
|
||||||
|
"phrase-abbreviator_option_language_choice_zh": "Cinese",
|
||||||
|
"phrase-abbreviator_option_add_number": "Aggiungi numero",
|
||||||
|
"phrase-abbreviator_option_add_number_description": "Aggiungi cifre casuali alla password",
|
||||||
|
"phrase-abbreviator_option_number_digits": "Numero di cifre",
|
||||||
|
"phrase-abbreviator_option_number_digits_description": "Quante cifre casuali aggiungere (1-4)",
|
||||||
|
"phrase-abbreviator_option_number_position": "Posizione del numero",
|
||||||
|
"phrase-abbreviator_option_number_position_description": "Posiziona il numero prima o dopo la password",
|
||||||
|
"phrase-abbreviator_option_number_position_choice_prepend": "Prima",
|
||||||
|
"phrase-abbreviator_option_number_position_choice_append": "Dopo",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_openai": "OpenAI",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_claude": "Claude",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_gemini": "Gemini",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_perplexity": "Perplexity",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-3.5-turbo": "GPT-3.5 Turbo",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4": "GPT-4",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4-turbo": "GPT-4 Turbo",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4o": "GPT-4 Omni",
|
||||||
|
"collapse_group_basic": "Opzioni di base",
|
||||||
|
"collapse_group_advanced": "Opzioni avanzate",
|
||||||
|
"collapse_group_ai": "Funzionalità IA",
|
||||||
|
"collapse_group_expert": "Opzioni esperte",
|
||||||
|
"collapse_group_character_set": "Set di caratteri",
|
||||||
|
"copied": "Copiato",
|
||||||
|
"pwgen_option_type_choice_custom": "Usa impostazione lunghezza",
|
||||||
|
"roulette": "Roulette",
|
||||||
|
"stop": "Ferma",
|
||||||
|
"roulette_interval": "Intervallo (s)",
|
||||||
|
"roulette_slow_down": "Aumenta intervallo (più lento)",
|
||||||
|
"roulette_speed_up": "Riduci intervallo (più veloce)"
|
||||||
|
}
|
||||||
+223
@@ -0,0 +1,223 @@
|
|||||||
|
{
|
||||||
|
"api_documentation": "APIドキュメント",
|
||||||
|
"app_title": "app_title",
|
||||||
|
"close": "閉じる",
|
||||||
|
"copied_to_clipboard": "クリップボードにコピーしました!",
|
||||||
|
"copy": "コピー",
|
||||||
|
"copy_failed": "コピーに失敗",
|
||||||
|
"count": "カウント",
|
||||||
|
"deutsch": "ドイツ語",
|
||||||
|
"dice_rolls": "サイコロの目",
|
||||||
|
"diceware_badge": "セキュリティ重視",
|
||||||
|
"diceware_description": "最大のセキュリティのために暗号学的に安全なサイコロを使用した本格的なEFF Diceware方式",
|
||||||
|
"diceware_option_show_dice_rolls": "Show Dice Rolls",
|
||||||
|
"diceware_option_show_dice_rolls_description": "学習用: 各単語の選択に使ったサイコロの目を表示します",
|
||||||
|
"diceware_option_uppercase_first": "Capitalize First Word",
|
||||||
|
"diceware_option_uppercase_first_description": "最初の単語だけを大文字にします(従来の Diceware 形式)",
|
||||||
|
"diceware_option_words": "Words",
|
||||||
|
"diceware_option_words_description": "安全性のため最低 6 単語を推奨します(77.5 ビットのエントロピー)。",
|
||||||
|
"diceware_title": "Diceware",
|
||||||
|
"english": "英語",
|
||||||
|
"entropy": "エントロピー",
|
||||||
|
"length": "長さ",
|
||||||
|
"details": "詳細",
|
||||||
|
"security_level": "セキュリティレベル",
|
||||||
|
"very_strong": "非常に強い",
|
||||||
|
"strong": "強い",
|
||||||
|
"adequate": "十分",
|
||||||
|
"weak": "弱い",
|
||||||
|
"very_weak": "非常に弱い",
|
||||||
|
"chars": "文字",
|
||||||
|
"error_generating_passwords": "パスワード生成エラー",
|
||||||
|
"espanol": "スペイン語",
|
||||||
|
"footer_text": "footer_text",
|
||||||
|
"francais": "フランス語",
|
||||||
|
"generate": "生成",
|
||||||
|
"generating": "生成中...",
|
||||||
|
"high-entropy_description": "最大エントロピーを持つ暗号学的に安全なランダムパスワード",
|
||||||
|
"high-entropy_option_include_lowercase": "Include Lowercase",
|
||||||
|
"high-entropy_option_include_numbers": "Include Numbers",
|
||||||
|
"high-entropy_option_include_symbols": "Include Symbols",
|
||||||
|
"high-entropy_option_include_uppercase": "Include Uppercase",
|
||||||
|
"high-entropy_option_length": "Length",
|
||||||
|
"high-entropy_title": "High Entropy",
|
||||||
|
"info_spaces_replaced_with_underscores": "選択した単語内のスペースはアンダースコア (_) に置き換えられます。",
|
||||||
|
"info_words_with_spaces_excluded": "スペースを含む単語は選択対象から除外されます。",
|
||||||
|
"italiano": "イタリア語",
|
||||||
|
"language": "言語:",
|
||||||
|
"loading_generators": "loading_generators",
|
||||||
|
"loading_password_generators": "パスワードジェネレーターを読み込み中...",
|
||||||
|
"native_group_explanation": "発音可能なパスワードを生成するための PwGen ネイティブ生成器オプションです。",
|
||||||
|
"native_pwgen": "ネイティブ設定",
|
||||||
|
"no_spaces_in_separator": "スペース処理が「変更」または「スキップ」の場合、区切り文字にはスペースを使わないでください。",
|
||||||
|
"passkey_description": "文字置換が統合されたAppleスタイルの発音可能なパスワード",
|
||||||
|
"passkey_option_syllables": "Syllables",
|
||||||
|
"passkey_title": "Passkey",
|
||||||
|
"password_generator": "パスワードジェネレーター",
|
||||||
|
"php_fallback": "ネイティブ pwgen が利用できない場合の PHP フォールバックコード。",
|
||||||
|
"portugues": "ポルトガル語",
|
||||||
|
"pwgen_badge": "強化版",
|
||||||
|
"pwgen_description": "読みやすいパターンを持つ古典的なpwgenスタイルのパスワード",
|
||||||
|
"pwgen_option_include_capitals": "大文字を含める",
|
||||||
|
"pwgen_option_include_capitals_description": "生成されるパスワードには大文字が含まれます。",
|
||||||
|
"pwgen_option_include_numbers": "数字を含める",
|
||||||
|
"pwgen_option_include_numbers_description": "生成されるパスワードには数字が含まれます。",
|
||||||
|
"pwgen_option_length": "長さ",
|
||||||
|
"pwgen_option_length_description": "区切り文字を含むパスワード全体の長さ(最小 4 文字)",
|
||||||
|
"pwgen_option_no_ambiguous": "曖昧な文字を避ける",
|
||||||
|
"pwgen_option_no_ambiguous_description": "読みやすさのため 0、O、l、1 などの文字を除外します",
|
||||||
|
"pwgen_option_pattern_complexity": "複雑さ",
|
||||||
|
"pwgen_option_pattern_complexity_description": "生成するパターンの複雑さを調整します。",
|
||||||
|
"pwgen_option_pattern_complexity_list_balanced": "バランス重視",
|
||||||
|
"pwgen_option_pattern_complexity_list_complex": "複雑なパターン",
|
||||||
|
"pwgen_option_pattern_complexity_list_simple": "シンプルなパターン",
|
||||||
|
"pwgen_option_secure_mode": "セキュアモード",
|
||||||
|
"pwgen_option_secure_mode_description": "発音可能な単語ではなく、完全にランダムなパスワードを生成します",
|
||||||
|
"pwgen_option_separator": "区切り文字",
|
||||||
|
"pwgen_option_separator_description": "音節を区切る文字。複数の文字が定義されている場合は、ランダムに選ばれた 1 つが使用されます。空欄の場合は区切りを行いません。",
|
||||||
|
"pwgen_option_syllable_count": "音節数",
|
||||||
|
"pwgen_option_syllable_count_description": "生成する音節数(区切り文字使用時のみ)",
|
||||||
|
"pwgen_title": "PWGen",
|
||||||
|
"results": "結果",
|
||||||
|
"schwyzerduetsch": "シュヴィーツァーデュッチ",
|
||||||
|
"secure_mode_disables_syllable_separation": "セキュアモードでは音節の分離は無効になります。",
|
||||||
|
"separator_method_info": "区切り方法の情報",
|
||||||
|
"single_space_allowed": "1 つのスペースのみ許可",
|
||||||
|
"success": "成功",
|
||||||
|
"syllable_group_explanation": "音節の説明",
|
||||||
|
"syllable_length_too_short": "音節が短すぎます",
|
||||||
|
"syllable_length_very_long": "音節が長すぎます",
|
||||||
|
"warning_short_word_limits_reduces_choices": "警告: 単語数の上限が低いと選択肢が減ります",
|
||||||
|
"warning_short_words_reduce_memorability": "警告: 短い単語は記憶しにくくなります",
|
||||||
|
"xkcdpass_badge": "記憶しやすさ重視",
|
||||||
|
"xkcdpass_description": "xkcdpassの説明",
|
||||||
|
"xkcdpass_option_addNumber": "Add Number",
|
||||||
|
"xkcdpass_option_max_word_length": "単語の最大長",
|
||||||
|
"xkcdpass_option_max_word_length_description": "0 = 制限なし、>0 = 単語ごとの最大文字数",
|
||||||
|
"xkcdpass_option_numberPosition": "Number Position",
|
||||||
|
"xkcdpass_option_numberPosition_list_beginning": "先頭",
|
||||||
|
"xkcdpass_option_numberPosition_list_end": "末尾",
|
||||||
|
"xkcdpass_option_numberPosition_list_random": "ランダム位置",
|
||||||
|
"xkcdpass_option_separator": "区切り文字",
|
||||||
|
"xkcdpass_option_separator_description": "1 文字または文字セット(ランダム選択)。複数文字セットではスペースは除外されます。",
|
||||||
|
"xkcdpass_option_space_handling": "スペースの扱い",
|
||||||
|
"xkcdpass_option_space_handling_description": "スペースを含む単語の扱い方",
|
||||||
|
"xkcdpass_option_space_handling_list_allow": "許可(スペースを残す)",
|
||||||
|
"xkcdpass_option_space_handling_list_modify": "変更(_ に置換)",
|
||||||
|
"xkcdpass_option_space_handling_list_skip": "スキップ(選択しない)",
|
||||||
|
"xkcdpass_option_wordlist": "単語一覧",
|
||||||
|
"xkcdpass_option_words": "単語",
|
||||||
|
"xkcdpass_option_words_description": "単語数(最小 2)",
|
||||||
|
"xkcdpass_title": "XKCD Pass",
|
||||||
|
"phraseabbreviator_number_zero": "零",
|
||||||
|
"phraseabbreviator_number_one": "一",
|
||||||
|
"phraseabbreviator_number_two": "二",
|
||||||
|
"phraseabbreviator_number_three": "三",
|
||||||
|
"phraseabbreviator_number_four": "四",
|
||||||
|
"phraseabbreviator_number_five": "五",
|
||||||
|
"phraseabbreviator_number_six": "六",
|
||||||
|
"phraseabbreviator_number_seven": "七",
|
||||||
|
"phraseabbreviator_number_eight": "八",
|
||||||
|
"phraseabbreviator_number_nine": "九",
|
||||||
|
"phraseabbreviator_number_ten": "十",
|
||||||
|
"phraseabbreviator_number_eleven": "十一",
|
||||||
|
"phraseabbreviator_number_twelve": "十二",
|
||||||
|
"phraseabbreviator_number_thirteen": "十三",
|
||||||
|
"phraseabbreviator_number_fourteen": "十四",
|
||||||
|
"phraseabbreviator_number_fifteen": "十五",
|
||||||
|
"phraseabbreviator_number_sixteen": "十六",
|
||||||
|
"phraseabbreviator_number_seventeen": "十七",
|
||||||
|
"phraseabbreviator_number_eighteen": "十八",
|
||||||
|
"phraseabbreviator_number_nineteen": "十九",
|
||||||
|
"phraseabbreviator_number_twenty": "二十",
|
||||||
|
"xkcdpass_option_use_ai_wordlist": "AI 単語リストを使用",
|
||||||
|
"xkcdpass_option_use_ai_wordlist_description": "AI モデルを使ってカスタム単語リストを生成します",
|
||||||
|
"xkcdpass_option_ai_wordlist_prompt": "AI 単語リストのプロンプト",
|
||||||
|
"xkcdpass_option_ai_wordlist_prompt_description": "プロンプトを完成させてください: \"少なくとも 5 文字の X 個の単語リストを作成し、…\"",
|
||||||
|
"xkcdpass_option_ai_provider": "AI プロバイダー",
|
||||||
|
"xkcdpass_option_ai_provider_description": "どの LLM プロバイダーを使用するか",
|
||||||
|
"phrase-abbreviator_option_use_ai_sentence": "AI 文を使用",
|
||||||
|
"phrase-abbreviator_option_use_ai_sentence_description": "AI にプロンプトから文章を生成させます",
|
||||||
|
"phrase-abbreviator_option_ai_prompt": "AI プロンプト",
|
||||||
|
"phrase-abbreviator_option_ai_prompt_description": "AI に記憶しやすい文章を生成させるためのプロンプト(最低 12 語)",
|
||||||
|
"phrase-abbreviator_option_ai_provider": "AI プロバイダー",
|
||||||
|
"phrase-abbreviator_option_ai_provider_description": "文章生成にどの LLM プロバイダーを使用するか",
|
||||||
|
"phrase-abbreviator_option_ai_model": "AI モデル",
|
||||||
|
"phrase-abbreviator_option_ai_model_description": "どの AI モデルを使用するか",
|
||||||
|
"high-entropy_option_length_description": "Total length of the generated password",
|
||||||
|
"high-entropy_option_include_uppercase_description": "Include uppercase letters (A-Z)",
|
||||||
|
"high-entropy_option_include_lowercase_description": "Include lowercase letters (a-z)",
|
||||||
|
"high-entropy_option_include_numbers_description": "Include numbers (0-9)",
|
||||||
|
"high-entropy_option_include_symbols_description": "Include symbols (e.g., !@#$%)",
|
||||||
|
"xkcdpass_option_wordlist_description": "Wordlist to use for generating the passphrase",
|
||||||
|
"xkcdpass_option_wordlist_choice_en": "English",
|
||||||
|
"xkcdpass_option_wordlist_choice_de": "Deutsch",
|
||||||
|
"xkcdpass_option_wordlist_choice_fr": "Français",
|
||||||
|
"xkcdpass_option_wordlist_choice_it": "Italiano",
|
||||||
|
"xkcdpass_option_wordlist_choice_es": "Español",
|
||||||
|
"xkcdpass_option_space_handling_choice_allow": "許可",
|
||||||
|
"xkcdpass_option_space_handling_choice_skip": "スキップ",
|
||||||
|
"xkcdpass_option_space_handling_choice_modify": "変更",
|
||||||
|
"xkcdpass_option_add_number": "数字を追加",
|
||||||
|
"xkcdpass_option_add_number_description": "Add random number to the passphrase",
|
||||||
|
"xkcdpass_option_number_position": "数字の位置",
|
||||||
|
"xkcdpass_option_number_position_description": "Where to place the random number",
|
||||||
|
"xkcdpass_option_number_position_choice_beginning": "先頭",
|
||||||
|
"xkcdpass_option_number_position_choice_end": "末尾",
|
||||||
|
"xkcdpass_option_number_position_choice_random": "ランダム",
|
||||||
|
"xkcdpass_option_ai_provider_choice_openai": "OpenAI",
|
||||||
|
"xkcdpass_option_ai_provider_choice_claude": "Claude",
|
||||||
|
"xkcdpass_option_ai_provider_choice_gemini": "Gemini",
|
||||||
|
"xkcdpass_option_ai_provider_choice_perplexity": "Perplexity",
|
||||||
|
"pwgen_option_type": "長さプリセット",
|
||||||
|
"pwgen_option_type_description": "任意の長さプリセットです。8/12/16 文字を選ぶと、上の「長さ」設定を上書きします。",
|
||||||
|
"pwgen_option_type_choice_8-char": "8 文字",
|
||||||
|
"pwgen_option_type_choice_12-char": "12 文字",
|
||||||
|
"pwgen_option_type_choice_16-char": "16 文字",
|
||||||
|
"passkey_option_syllables_description": "Number of pronounceable syllables in the passkey",
|
||||||
|
"phrase-abbreviator_option_sentence": "文章",
|
||||||
|
"phrase-abbreviator_option_sentence_description": "User-supplied sentence (minimum 8 words, must start with capital letter)",
|
||||||
|
"phrase-abbreviator_option_character_extraction": "文字抽出",
|
||||||
|
"phrase-abbreviator_option_character_extraction_description": "Method for extracting characters from each word",
|
||||||
|
"phrase-abbreviator_option_character_extraction_choice_single": "先頭文字のみ",
|
||||||
|
"phrase-abbreviator_option_character_extraction_choice_dual": "先頭文字 + 次の母音/子音",
|
||||||
|
"phrase-abbreviator_option_language": "言語",
|
||||||
|
"phrase-abbreviator_option_language_description": "Language for number word conversion",
|
||||||
|
"phrase-abbreviator_option_language_choice_en": "English",
|
||||||
|
"phrase-abbreviator_option_language_choice_de": "ドイツ語",
|
||||||
|
"phrase-abbreviator_option_language_choice_de_CH": "シュワイツァー・ドイツ語",
|
||||||
|
"phrase-abbreviator_option_language_choice_fr": "フランス語",
|
||||||
|
"phrase-abbreviator_option_language_choice_it": "イタリア語",
|
||||||
|
"phrase-abbreviator_option_language_choice_es": "スペイン語",
|
||||||
|
"phrase-abbreviator_option_language_choice_pt": "ポルトガル語",
|
||||||
|
"phrase-abbreviator_option_language_choice_ja": "日本語",
|
||||||
|
"phrase-abbreviator_option_language_choice_zh": "中国語",
|
||||||
|
"phrase-abbreviator_option_add_number": "数字を追加",
|
||||||
|
"phrase-abbreviator_option_add_number_description": "パスワードにランダムな数字を追加",
|
||||||
|
"phrase-abbreviator_option_number_digits": "数字の個数",
|
||||||
|
"phrase-abbreviator_option_number_digits_description": "追加するランダム数字の数(1〜4)",
|
||||||
|
"phrase-abbreviator_option_number_position": "数字の位置",
|
||||||
|
"phrase-abbreviator_option_number_position_description": "数字をパスワードの前または後に配置",
|
||||||
|
"phrase-abbreviator_option_number_position_choice_prepend": "前",
|
||||||
|
"phrase-abbreviator_option_number_position_choice_append": "後",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_openai": "OpenAI",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_claude": "Claude",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_gemini": "Gemini",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_perplexity": "Perplexity",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-3.5-turbo": "GPT-3.5 Turbo",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4": "GPT-4",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4-turbo": "GPT-4 Turbo",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4o": "GPT-4 Omni",
|
||||||
|
"collapse_group_basic": "基本オプション",
|
||||||
|
"collapse_group_advanced": "詳細オプション",
|
||||||
|
"collapse_group_ai": "AI 機能",
|
||||||
|
"collapse_group_expert": "上級オプション",
|
||||||
|
"collapse_group_character_set": "文字セット",
|
||||||
|
"copied": "コピー済み",
|
||||||
|
"pwgen_option_type_choice_custom": "長さ設定を使用",
|
||||||
|
"roulette": "ルーレット",
|
||||||
|
"stop": "停止",
|
||||||
|
"roulette_interval": "間隔 (秒)",
|
||||||
|
"roulette_slow_down": "間隔を増やす(遅く)",
|
||||||
|
"roulette_speed_up": "間隔を減らす(速く)"
|
||||||
|
}
|
||||||
+223
@@ -0,0 +1,223 @@
|
|||||||
|
{
|
||||||
|
"api_documentation": "Documentação API",
|
||||||
|
"app_title": "Gerador de Senhas",
|
||||||
|
"close": "Fechar",
|
||||||
|
"copied_to_clipboard": "Copiado para a área de transferência!",
|
||||||
|
"copy": "Copiar",
|
||||||
|
"copy_failed": "Falha ao copiar",
|
||||||
|
"count": "Quantidade",
|
||||||
|
"deutsch": "Deutsch",
|
||||||
|
"dice_rolls": "Lançamentos de dados",
|
||||||
|
"diceware_badge": "Foco na Segurança",
|
||||||
|
"diceware_description": "Método EFF Diceware autêntico usando jogadas de dados criptograficamente seguras para máxima segurança",
|
||||||
|
"diceware_option_show_dice_rolls": "Show Dice Rolls",
|
||||||
|
"diceware_option_show_dice_rolls_description": "Educational: display the dice rolls used to select each word",
|
||||||
|
"diceware_option_uppercase_first": "Capitalize First Word",
|
||||||
|
"diceware_option_uppercase_first_description": "Capitalize only the first word (traditional Diceware style)",
|
||||||
|
"diceware_option_words": "Words",
|
||||||
|
"diceware_option_words_description": "Minimum 6 words recommended for security (77.5 bits entropy)",
|
||||||
|
"diceware_title": "Diceware",
|
||||||
|
"english": "Inglês",
|
||||||
|
"entropy": "Entropia",
|
||||||
|
"length": "Comprimento",
|
||||||
|
"details": "Detalhes",
|
||||||
|
"security_level": "Nível de segurança",
|
||||||
|
"very_strong": "Muito forte",
|
||||||
|
"strong": "Forte",
|
||||||
|
"adequate": "Adequado",
|
||||||
|
"weak": "Fraca",
|
||||||
|
"very_weak": "Muito fraca",
|
||||||
|
"chars": "caracteres",
|
||||||
|
"error_generating_passwords": "Erro ao gerar senhas",
|
||||||
|
"espanol": "Español",
|
||||||
|
"footer_text": "texto do rodapé",
|
||||||
|
"francais": "Français",
|
||||||
|
"generate": "Gerar",
|
||||||
|
"generating": "Gerando...",
|
||||||
|
"high-entropy_description": "Senhas aleatórias criptograficamente seguras com entropia máxima",
|
||||||
|
"high-entropy_option_include_lowercase": "Include Lowercase",
|
||||||
|
"high-entropy_option_include_numbers": "Include Numbers",
|
||||||
|
"high-entropy_option_include_symbols": "Include Symbols",
|
||||||
|
"high-entropy_option_include_uppercase": "Include Uppercase",
|
||||||
|
"high-entropy_option_length": "Length",
|
||||||
|
"high-entropy_title": "High Entropy",
|
||||||
|
"info_spaces_replaced_with_underscores": "Espaços nas palavras selecionadas são substituídos por underscores (_).",
|
||||||
|
"info_words_with_spaces_excluded": "Palavras que contêm espaços são excluídas da seleção.",
|
||||||
|
"italiano": "Italiano",
|
||||||
|
"language": "Idioma:",
|
||||||
|
"loading_generators": "Carregando geradores",
|
||||||
|
"loading_password_generators": "Carregando geradores de senhas...",
|
||||||
|
"native_group_explanation": "Opções nativas do gerador PwGen para gerar senhas pronunciáveis.",
|
||||||
|
"native_pwgen": "Opções nativas",
|
||||||
|
"no_spaces_in_separator": "Não use espaços no separador quando o tratamento de espaço estiver definido como “modificar” ou “pular”.",
|
||||||
|
"passkey_description": "Senhas pronunciáveis estilo Apple com substituições de caracteres integradas",
|
||||||
|
"passkey_option_syllables": "Syllables",
|
||||||
|
"passkey_title": "Passkey",
|
||||||
|
"password_generator": "Gerador de senhas",
|
||||||
|
"php_fallback": "Código de fallback em PHP quando o pwgen nativo não está disponível.",
|
||||||
|
"portugues": "Português",
|
||||||
|
"pwgen_badge": "Melhorado",
|
||||||
|
"pwgen_description": "Senhas estilo pwgen clássico com padrões legíveis",
|
||||||
|
"pwgen_option_include_capitals": "Incluir maiúsculas",
|
||||||
|
"pwgen_option_include_capitals_description": "As senhas geradas conterão letras maiúsculas.",
|
||||||
|
"pwgen_option_include_numbers": "Incluir números",
|
||||||
|
"pwgen_option_include_numbers_description": "As senhas geradas conterão dígitos.",
|
||||||
|
"pwgen_option_length": "Comprimento",
|
||||||
|
"pwgen_option_length_description": "Comprimento total da senha incluindo separadores (mínimo de 4 caracteres)",
|
||||||
|
"pwgen_option_no_ambiguous": "Sem caracteres ambíguos",
|
||||||
|
"pwgen_option_no_ambiguous_description": "Exclui caracteres como 0, O, l, 1, etc. para melhorar a legibilidade",
|
||||||
|
"pwgen_option_pattern_complexity": "Padrão complexo",
|
||||||
|
"pwgen_option_pattern_complexity_description": "Controla a complexidade dos padrões gerados.",
|
||||||
|
"pwgen_option_pattern_complexity_list_balanced": "Complexidade equilibrada",
|
||||||
|
"pwgen_option_pattern_complexity_list_complex": "Padrões complexos",
|
||||||
|
"pwgen_option_pattern_complexity_list_simple": "Padrões simples",
|
||||||
|
"pwgen_option_secure_mode": "Modo seguro",
|
||||||
|
"pwgen_option_secure_mode_description": "Gera senhas totalmente aleatórias em vez de pronunciáveis",
|
||||||
|
"pwgen_option_separator": "Separador",
|
||||||
|
"pwgen_option_separator_description": "Caractere(s) para separar sílabas. Se vários caracteres forem definidos, os trechos são separados por um caractere escolhido aleatoriamente. Se estiver vazio, não há separação.",
|
||||||
|
"pwgen_option_syllable_count": "Número de sílabas",
|
||||||
|
"pwgen_option_syllable_count_description": "Número de sílabas a gerar (usado somente com separador)",
|
||||||
|
"pwgen_title": "PWGen",
|
||||||
|
"results": "Resultados",
|
||||||
|
"schwyzerduetsch": "Alemão Suíço",
|
||||||
|
"secure_mode_disables_syllable_separation": "O modo seguro desativa a separação por sílabas.",
|
||||||
|
"separator_method_info": "Informações sobre o método de separação",
|
||||||
|
"single_space_allowed": "Permite apenas um espaço",
|
||||||
|
"success": "Sucesso",
|
||||||
|
"syllable_group_explanation": "Explicação das sílabas",
|
||||||
|
"syllable_length_too_short": "A sílaba é muito curta",
|
||||||
|
"syllable_length_very_long": "A sílaba é muito longa",
|
||||||
|
"warning_short_word_limits_reduces_choices": "Aviso: limites de palavras curtas reduzem as opções",
|
||||||
|
"warning_short_words_reduce_memorability": "Aviso: palavras curtas reduzem a memorização",
|
||||||
|
"xkcdpass_badge": "Foco na Memorabilidade",
|
||||||
|
"xkcdpass_description": "Descrição xkcdpass",
|
||||||
|
"xkcdpass_option_addNumber": "Add Number",
|
||||||
|
"xkcdpass_option_max_word_length": "Comprimento máximo da palavra",
|
||||||
|
"xkcdpass_option_max_word_length_description": "0 = no limit, >0 = maximum characters per word",
|
||||||
|
"xkcdpass_option_numberPosition": "Number Position",
|
||||||
|
"xkcdpass_option_numberPosition_list_beginning": "No início",
|
||||||
|
"xkcdpass_option_numberPosition_list_end": "No fim",
|
||||||
|
"xkcdpass_option_numberPosition_list_random": "Posição aleatória",
|
||||||
|
"xkcdpass_option_separator": "Separador",
|
||||||
|
"xkcdpass_option_separator_description": "Single character or character set (random selection). Spaces excluded from multi-character sets.",
|
||||||
|
"xkcdpass_option_space_handling": "Tratamento de espaços",
|
||||||
|
"xkcdpass_option_space_handling_description": "Como lidar com palavras contendo espaços",
|
||||||
|
"xkcdpass_option_space_handling_list_allow": "Permitir (manter espaços)",
|
||||||
|
"xkcdpass_option_space_handling_list_modify": "Modificar (substituir por _)",
|
||||||
|
"xkcdpass_option_space_handling_list_skip": "Pular (não selecionar)",
|
||||||
|
"xkcdpass_option_wordlist": "Lista de palavras",
|
||||||
|
"xkcdpass_option_words": "Palavras",
|
||||||
|
"xkcdpass_option_words_description": "Number of words (minimal 2)",
|
||||||
|
"xkcdpass_title": "XKCD Pass",
|
||||||
|
"phraseabbreviator_number_zero": "zero",
|
||||||
|
"phraseabbreviator_number_one": "um",
|
||||||
|
"phraseabbreviator_number_two": "dois",
|
||||||
|
"phraseabbreviator_number_three": "três",
|
||||||
|
"phraseabbreviator_number_four": "quatro",
|
||||||
|
"phraseabbreviator_number_five": "cinco",
|
||||||
|
"phraseabbreviator_number_six": "seis",
|
||||||
|
"phraseabbreviator_number_seven": "sete",
|
||||||
|
"phraseabbreviator_number_eight": "oito",
|
||||||
|
"phraseabbreviator_number_nine": "nove",
|
||||||
|
"phraseabbreviator_number_ten": "dez",
|
||||||
|
"phraseabbreviator_number_eleven": "onze",
|
||||||
|
"phraseabbreviator_number_twelve": "doze",
|
||||||
|
"phraseabbreviator_number_thirteen": "treze",
|
||||||
|
"phraseabbreviator_number_fourteen": "catorze",
|
||||||
|
"phraseabbreviator_number_fifteen": "quinze",
|
||||||
|
"phraseabbreviator_number_sixteen": "dezesseis",
|
||||||
|
"phraseabbreviator_number_seventeen": "dezessete",
|
||||||
|
"phraseabbreviator_number_eighteen": "dezoito",
|
||||||
|
"phraseabbreviator_number_nineteen": "dezenove",
|
||||||
|
"phraseabbreviator_number_twenty": "vinte",
|
||||||
|
"xkcdpass_option_use_ai_wordlist": "Usar lista de palavras com IA",
|
||||||
|
"xkcdpass_option_use_ai_wordlist_description": "Gera uma lista de palavras personalizada usando um modelo de IA",
|
||||||
|
"xkcdpass_option_ai_wordlist_prompt": "Prompt da lista de palavras de IA",
|
||||||
|
"xkcdpass_option_ai_wordlist_prompt_description": "Complete o prompt: \"Crie uma lista de X palavras com pelo menos 5 caracteres, que...\"",
|
||||||
|
"xkcdpass_option_ai_provider": "Provedor de IA",
|
||||||
|
"xkcdpass_option_ai_provider_description": "Qual provedor LLM usar",
|
||||||
|
"phrase-abbreviator_option_use_ai_sentence": "Usar frase de IA",
|
||||||
|
"phrase-abbreviator_option_use_ai_sentence_description": "Permite que a IA gere uma frase a partir da sua instrução",
|
||||||
|
"phrase-abbreviator_option_ai_prompt": "Prompt de IA",
|
||||||
|
"phrase-abbreviator_option_ai_prompt_description": "Prompt para a IA gerar uma frase memorável (mínimo de 12 palavras)",
|
||||||
|
"phrase-abbreviator_option_ai_provider": "Provedor de IA",
|
||||||
|
"phrase-abbreviator_option_ai_provider_description": "Qual provedor LLM usar para gerar a frase",
|
||||||
|
"phrase-abbreviator_option_ai_model": "Modelo de IA",
|
||||||
|
"phrase-abbreviator_option_ai_model_description": "Qual modelo de IA usar",
|
||||||
|
"high-entropy_option_length_description": "Total length of the generated password",
|
||||||
|
"high-entropy_option_include_uppercase_description": "Include uppercase letters (A-Z)",
|
||||||
|
"high-entropy_option_include_lowercase_description": "Include lowercase letters (a-z)",
|
||||||
|
"high-entropy_option_include_numbers_description": "Include numbers (0-9)",
|
||||||
|
"high-entropy_option_include_symbols_description": "Include symbols (e.g., !@#$%)",
|
||||||
|
"xkcdpass_option_wordlist_description": "Wordlist to use for generating the passphrase",
|
||||||
|
"xkcdpass_option_wordlist_choice_en": "English",
|
||||||
|
"xkcdpass_option_wordlist_choice_de": "Deutsch",
|
||||||
|
"xkcdpass_option_wordlist_choice_fr": "Français",
|
||||||
|
"xkcdpass_option_wordlist_choice_it": "Italiano",
|
||||||
|
"xkcdpass_option_wordlist_choice_es": "Español",
|
||||||
|
"xkcdpass_option_space_handling_choice_allow": "Permitir",
|
||||||
|
"xkcdpass_option_space_handling_choice_skip": "Pular",
|
||||||
|
"xkcdpass_option_space_handling_choice_modify": "Modificar",
|
||||||
|
"xkcdpass_option_add_number": "Adicionar número",
|
||||||
|
"xkcdpass_option_add_number_description": "Add random number to the passphrase",
|
||||||
|
"xkcdpass_option_number_position": "Posição do número",
|
||||||
|
"xkcdpass_option_number_position_description": "Where to place the random number",
|
||||||
|
"xkcdpass_option_number_position_choice_beginning": "No início",
|
||||||
|
"xkcdpass_option_number_position_choice_end": "No fim",
|
||||||
|
"xkcdpass_option_number_position_choice_random": "Posição aleatória",
|
||||||
|
"xkcdpass_option_ai_provider_choice_openai": "OpenAI",
|
||||||
|
"xkcdpass_option_ai_provider_choice_claude": "Claude",
|
||||||
|
"xkcdpass_option_ai_provider_choice_gemini": "Gemini",
|
||||||
|
"xkcdpass_option_ai_provider_choice_perplexity": "Perplexity",
|
||||||
|
"pwgen_option_type": "Predefinição de comprimento",
|
||||||
|
"pwgen_option_type_description": "Predefinição opcional de comprimento. Se definida para 8/12/16 caracteres, substitui o valor de Comprimento acima.",
|
||||||
|
"pwgen_option_type_choice_8-char": "8 caracteres",
|
||||||
|
"pwgen_option_type_choice_12-char": "12 caracteres",
|
||||||
|
"pwgen_option_type_choice_16-char": "16 caracteres",
|
||||||
|
"passkey_option_syllables_description": "Number of pronounceable syllables in the passkey",
|
||||||
|
"phrase-abbreviator_option_sentence": "Frase",
|
||||||
|
"phrase-abbreviator_option_sentence_description": "User-supplied sentence (minimum 8 words, must start with capital letter)",
|
||||||
|
"phrase-abbreviator_option_character_extraction": "Extração de caracteres",
|
||||||
|
"phrase-abbreviator_option_character_extraction_description": "Method for extracting characters from each word",
|
||||||
|
"phrase-abbreviator_option_character_extraction_choice_single": "Apenas primeira letra",
|
||||||
|
"phrase-abbreviator_option_character_extraction_choice_dual": "Primeira letra + próxima vogal/consoante",
|
||||||
|
"phrase-abbreviator_option_language": "Idioma",
|
||||||
|
"phrase-abbreviator_option_language_description": "Language for number word conversion",
|
||||||
|
"phrase-abbreviator_option_language_choice_en": "English",
|
||||||
|
"phrase-abbreviator_option_language_choice_de": "Alemão",
|
||||||
|
"phrase-abbreviator_option_language_choice_de_CH": "Suíço-alemão",
|
||||||
|
"phrase-abbreviator_option_language_choice_fr": "Francês",
|
||||||
|
"phrase-abbreviator_option_language_choice_it": "Italiano",
|
||||||
|
"phrase-abbreviator_option_language_choice_es": "Espanhol",
|
||||||
|
"phrase-abbreviator_option_language_choice_pt": "Português",
|
||||||
|
"phrase-abbreviator_option_language_choice_ja": "Japonês",
|
||||||
|
"phrase-abbreviator_option_language_choice_zh": "Chinês",
|
||||||
|
"phrase-abbreviator_option_add_number": "Adicionar número",
|
||||||
|
"phrase-abbreviator_option_add_number_description": "Adicionar dígitos aleatórios à senha",
|
||||||
|
"phrase-abbreviator_option_number_digits": "Número de dígitos",
|
||||||
|
"phrase-abbreviator_option_number_digits_description": "Quantos dígitos aleatórios adicionar (1-4)",
|
||||||
|
"phrase-abbreviator_option_number_position": "Posição do número",
|
||||||
|
"phrase-abbreviator_option_number_position_description": "Colocar o número antes ou depois da senha",
|
||||||
|
"phrase-abbreviator_option_number_position_choice_prepend": "Antes",
|
||||||
|
"phrase-abbreviator_option_number_position_choice_append": "Depois",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_openai": "OpenAI",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_claude": "Claude",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_gemini": "Gemini",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_perplexity": "Perplexity",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-3.5-turbo": "GPT-3.5 Turbo",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4": "GPT-4",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4-turbo": "GPT-4 Turbo",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4o": "GPT-4 Omni",
|
||||||
|
"collapse_group_basic": "Opções básicas",
|
||||||
|
"collapse_group_advanced": "Opções avançadas",
|
||||||
|
"collapse_group_ai": "Recursos de IA",
|
||||||
|
"collapse_group_expert": "Opções avançadas de especialista",
|
||||||
|
"collapse_group_character_set": "Conjunto de caracteres",
|
||||||
|
"copied": "Copiado",
|
||||||
|
"pwgen_option_type_choice_custom": "Usar configuração de comprimento",
|
||||||
|
"roulette": "Roleta",
|
||||||
|
"stop": "Parar",
|
||||||
|
"roulette_interval": "Intervalo (s)",
|
||||||
|
"roulette_slow_down": "Aumentar intervalo (mais lento)",
|
||||||
|
"roulette_speed_up": "Reduzir intervalo (mais rápido)"
|
||||||
|
}
|
||||||
+223
@@ -0,0 +1,223 @@
|
|||||||
|
{
|
||||||
|
"api_documentation": "API文档",
|
||||||
|
"app_title": "密码生成器",
|
||||||
|
"close": "关闭",
|
||||||
|
"copied_to_clipboard": "Copied to clipboard!",
|
||||||
|
"copy": "复制",
|
||||||
|
"copy_failed": "复制失败",
|
||||||
|
"count": "数量",
|
||||||
|
"deutsch": "Deutsch",
|
||||||
|
"dice_rolls": "骰子结果",
|
||||||
|
"diceware_badge": "安全重点",
|
||||||
|
"diceware_description": "真正的EFF Diceware方法",
|
||||||
|
"diceware_option_show_dice_rolls": "Show Dice Rolls",
|
||||||
|
"diceware_option_show_dice_rolls_description": "Educational: display the dice rolls used to select each word",
|
||||||
|
"diceware_option_uppercase_first": "Capitalize First Word",
|
||||||
|
"diceware_option_uppercase_first_description": "Capitalize only the first word (traditional Diceware style)",
|
||||||
|
"diceware_option_words": "Words",
|
||||||
|
"diceware_option_words_description": "Minimum 6 words recommended for security (77.5 bits entropy)",
|
||||||
|
"diceware_title": "Diceware",
|
||||||
|
"english": "英语",
|
||||||
|
"entropy": "熵",
|
||||||
|
"length": "长度",
|
||||||
|
"details": "详细信息",
|
||||||
|
"security_level": "安全等级",
|
||||||
|
"very_strong": "非常强",
|
||||||
|
"strong": "强",
|
||||||
|
"adequate": "适中",
|
||||||
|
"weak": "弱",
|
||||||
|
"very_weak": "非常弱",
|
||||||
|
"chars": "字符",
|
||||||
|
"error_generating_passwords": "生成密码时出错",
|
||||||
|
"espanol": "西班牙语",
|
||||||
|
"footer_text": "页脚文本",
|
||||||
|
"francais": "法语",
|
||||||
|
"generate": "生成",
|
||||||
|
"generating": "Generating...",
|
||||||
|
"high-entropy_description": "高熵描述",
|
||||||
|
"high-entropy_option_include_lowercase": "Include Lowercase",
|
||||||
|
"high-entropy_option_include_numbers": "Include Numbers",
|
||||||
|
"high-entropy_option_include_symbols": "Include Symbols",
|
||||||
|
"high-entropy_option_include_uppercase": "Include Uppercase",
|
||||||
|
"high-entropy_option_length": "Length",
|
||||||
|
"high-entropy_title": "High Entropy",
|
||||||
|
"info_spaces_replaced_with_underscores": "所选单词中的空格会替换为下划线 (_) 。",
|
||||||
|
"info_words_with_spaces_excluded": "包含空格的单词会被排除在选择范围之外。",
|
||||||
|
"italiano": "意大利语",
|
||||||
|
"language": "Language:",
|
||||||
|
"loading_generators": "正在加载生成器",
|
||||||
|
"loading_password_generators": "Loading password generators...",
|
||||||
|
"native_group_explanation": "用于生成可发音密码的 PwGen 原生生成器选项。",
|
||||||
|
"native_pwgen": "原生选项",
|
||||||
|
"no_spaces_in_separator": "当空格处理设为“修改”或“跳过”时,不要在分隔符中使用空格。",
|
||||||
|
"passkey_description": "通行密钥描述",
|
||||||
|
"passkey_option_syllables": "Syllables",
|
||||||
|
"passkey_title": "Passkey",
|
||||||
|
"password_generator": "Password Generator",
|
||||||
|
"php_fallback": "在原生 pwgen 不可用时的 PHP 回退代码。",
|
||||||
|
"portugues": "葡萄牙语",
|
||||||
|
"pwgen_badge": "增强版",
|
||||||
|
"pwgen_description": "PWGen描述",
|
||||||
|
"pwgen_option_include_capitals": "包含大写字母",
|
||||||
|
"pwgen_option_include_capitals_description": "生成的密码将包含大写字母。",
|
||||||
|
"pwgen_option_include_numbers": "包含数字",
|
||||||
|
"pwgen_option_include_numbers_description": "生成的密码将包含数字。",
|
||||||
|
"pwgen_option_length": "长度",
|
||||||
|
"pwgen_option_length_description": "密码总长度,包括分隔符(最少 4 个字符)",
|
||||||
|
"pwgen_option_no_ambiguous": "不含歧义字符",
|
||||||
|
"pwgen_option_no_ambiguous_description": "排除 0、O、l、1 等字符,以提高可读性",
|
||||||
|
"pwgen_option_pattern_complexity": "复杂模式",
|
||||||
|
"pwgen_option_pattern_complexity_description": "控制生成模式的复杂度。",
|
||||||
|
"pwgen_option_pattern_complexity_list_balanced": "平衡复杂度",
|
||||||
|
"pwgen_option_pattern_complexity_list_complex": "复杂模式",
|
||||||
|
"pwgen_option_pattern_complexity_list_simple": "简单模式",
|
||||||
|
"pwgen_option_secure_mode": "安全模式",
|
||||||
|
"pwgen_option_secure_mode_description": "生成完全随机的密码,而不是可发音的单词",
|
||||||
|
"pwgen_option_separator": "分隔符",
|
||||||
|
"pwgen_option_separator_description": "用于分隔音节的字符。如果定义了多个字符,则随机选择其中一个作为分隔符。为空时不进行分隔。",
|
||||||
|
"pwgen_option_syllable_count": "音节数",
|
||||||
|
"pwgen_option_syllable_count_description": "要生成的音节数(仅在使用分隔符时生效)",
|
||||||
|
"pwgen_title": "PWGen",
|
||||||
|
"results": "结果",
|
||||||
|
"schwyzerduetsch": "瑞士德语",
|
||||||
|
"secure_mode_disables_syllable_separation": "安全模式会禁用音节分隔。",
|
||||||
|
"separator_method_info": "分隔方法说明",
|
||||||
|
"single_space_allowed": "仅允许单个空格",
|
||||||
|
"success": "成功",
|
||||||
|
"syllable_group_explanation": "音节说明",
|
||||||
|
"syllable_length_too_short": "音节太短",
|
||||||
|
"syllable_length_very_long": "音节太长",
|
||||||
|
"warning_short_word_limits_reduces_choices": "警告:较短的单词限制会减少可选项",
|
||||||
|
"warning_short_words_reduce_memorability": "警告:短单词会降低可记忆性",
|
||||||
|
"xkcdpass_badge": "记忆性重点",
|
||||||
|
"xkcdpass_description": "XKCDPass描述",
|
||||||
|
"xkcdpass_option_addNumber": "Add Number",
|
||||||
|
"xkcdpass_option_max_word_length": "单词最大长度",
|
||||||
|
"xkcdpass_option_max_word_length_description": "0 = no limit, >0 = maximum characters per word",
|
||||||
|
"xkcdpass_option_numberPosition": "Number Position",
|
||||||
|
"xkcdpass_option_numberPosition_list_beginning": "开头",
|
||||||
|
"xkcdpass_option_numberPosition_list_end": "结尾",
|
||||||
|
"xkcdpass_option_numberPosition_list_random": "随机位置",
|
||||||
|
"xkcdpass_option_separator": "分隔符",
|
||||||
|
"xkcdpass_option_separator_description": "Single character or character set (random selection). Spaces excluded from multi-character sets.",
|
||||||
|
"xkcdpass_option_space_handling": "空格处理",
|
||||||
|
"xkcdpass_option_space_handling_description": "如何处理包含空格的单词",
|
||||||
|
"xkcdpass_option_space_handling_list_allow": "允许(保留空格)",
|
||||||
|
"xkcdpass_option_space_handling_list_modify": "修改(替换为 _)",
|
||||||
|
"xkcdpass_option_space_handling_list_skip": "跳过(不选择)",
|
||||||
|
"xkcdpass_option_wordlist": "单词列表",
|
||||||
|
"xkcdpass_option_words": "单词",
|
||||||
|
"xkcdpass_option_words_description": "Number of words (minimal 2)",
|
||||||
|
"xkcdpass_title": "XKCD Pass",
|
||||||
|
"phraseabbreviator_number_zero": "零",
|
||||||
|
"phraseabbreviator_number_one": "一",
|
||||||
|
"phraseabbreviator_number_two": "二",
|
||||||
|
"phraseabbreviator_number_three": "三",
|
||||||
|
"phraseabbreviator_number_four": "四",
|
||||||
|
"phraseabbreviator_number_five": "五",
|
||||||
|
"phraseabbreviator_number_six": "六",
|
||||||
|
"phraseabbreviator_number_seven": "七",
|
||||||
|
"phraseabbreviator_number_eight": "八",
|
||||||
|
"phraseabbreviator_number_nine": "九",
|
||||||
|
"phraseabbreviator_number_ten": "十",
|
||||||
|
"phraseabbreviator_number_eleven": "十一",
|
||||||
|
"phraseabbreviator_number_twelve": "十二",
|
||||||
|
"phraseabbreviator_number_thirteen": "十三",
|
||||||
|
"phraseabbreviator_number_fourteen": "十四",
|
||||||
|
"phraseabbreviator_number_fifteen": "十五",
|
||||||
|
"phraseabbreviator_number_sixteen": "十六",
|
||||||
|
"phraseabbreviator_number_seventeen": "十七",
|
||||||
|
"phraseabbreviator_number_eighteen": "十八",
|
||||||
|
"phraseabbreviator_number_nineteen": "十九",
|
||||||
|
"phraseabbreviator_number_twenty": "二十",
|
||||||
|
"xkcdpass_option_use_ai_wordlist": "使用 AI 词表",
|
||||||
|
"xkcdpass_option_use_ai_wordlist_description": "使用 AI 模型生成自定义词表",
|
||||||
|
"xkcdpass_option_ai_wordlist_prompt": "AI 词表提示词",
|
||||||
|
"xkcdpass_option_ai_wordlist_prompt_description": "完成提示词:\"创建一个至少 5 个字符的 X 个单词列表,要求...\"",
|
||||||
|
"xkcdpass_option_ai_provider": "AI 提供商",
|
||||||
|
"xkcdpass_option_ai_provider_description": "使用哪个 LLM 提供商",
|
||||||
|
"phrase-abbreviator_option_use_ai_sentence": "使用 AI 句子",
|
||||||
|
"phrase-abbreviator_option_use_ai_sentence_description": "让 AI 根据你的提示生成句子",
|
||||||
|
"phrase-abbreviator_option_ai_prompt": "AI 提示词",
|
||||||
|
"phrase-abbreviator_option_ai_prompt_description": "让 AI 生成易记句子的提示词(至少 12 个词)",
|
||||||
|
"phrase-abbreviator_option_ai_provider": "AI 提供商",
|
||||||
|
"phrase-abbreviator_option_ai_provider_description": "用于句子生成的 LLM 提供商",
|
||||||
|
"phrase-abbreviator_option_ai_model": "AI 模型",
|
||||||
|
"phrase-abbreviator_option_ai_model_description": "使用哪个 AI 模型",
|
||||||
|
"high-entropy_option_length_description": "Total length of the generated password",
|
||||||
|
"high-entropy_option_include_uppercase_description": "Include uppercase letters (A-Z)",
|
||||||
|
"high-entropy_option_include_lowercase_description": "Include lowercase letters (a-z)",
|
||||||
|
"high-entropy_option_include_numbers_description": "Include numbers (0-9)",
|
||||||
|
"high-entropy_option_include_symbols_description": "Include symbols (e.g., !@#$%)",
|
||||||
|
"xkcdpass_option_wordlist_description": "Wordlist to use for generating the passphrase",
|
||||||
|
"xkcdpass_option_wordlist_choice_en": "English",
|
||||||
|
"xkcdpass_option_wordlist_choice_de": "Deutsch",
|
||||||
|
"xkcdpass_option_wordlist_choice_fr": "Français",
|
||||||
|
"xkcdpass_option_wordlist_choice_it": "Italiano",
|
||||||
|
"xkcdpass_option_wordlist_choice_es": "Español",
|
||||||
|
"xkcdpass_option_space_handling_choice_allow": "允许",
|
||||||
|
"xkcdpass_option_space_handling_choice_skip": "跳过",
|
||||||
|
"xkcdpass_option_space_handling_choice_modify": "修改",
|
||||||
|
"xkcdpass_option_add_number": "添加数字",
|
||||||
|
"xkcdpass_option_add_number_description": "Add random number to the passphrase",
|
||||||
|
"xkcdpass_option_number_position": "数字位置",
|
||||||
|
"xkcdpass_option_number_position_description": "Where to place the random number",
|
||||||
|
"xkcdpass_option_number_position_choice_beginning": "开头",
|
||||||
|
"xkcdpass_option_number_position_choice_end": "结尾",
|
||||||
|
"xkcdpass_option_number_position_choice_random": "随机位置",
|
||||||
|
"xkcdpass_option_ai_provider_choice_openai": "OpenAI",
|
||||||
|
"xkcdpass_option_ai_provider_choice_claude": "Claude",
|
||||||
|
"xkcdpass_option_ai_provider_choice_gemini": "Gemini",
|
||||||
|
"xkcdpass_option_ai_provider_choice_perplexity": "Perplexity",
|
||||||
|
"pwgen_option_type": "长度预设",
|
||||||
|
"pwgen_option_type_description": "可选的长度预设。若设置为 8/12/16 个字符,将覆盖上方的长度值。",
|
||||||
|
"pwgen_option_type_choice_8-char": "8 个字符",
|
||||||
|
"pwgen_option_type_choice_12-char": "12 个字符",
|
||||||
|
"pwgen_option_type_choice_16-char": "16 个字符",
|
||||||
|
"passkey_option_syllables_description": "Number of pronounceable syllables in the passkey",
|
||||||
|
"phrase-abbreviator_option_sentence": "句子",
|
||||||
|
"phrase-abbreviator_option_sentence_description": "User-supplied sentence (minimum 8 words, must start with capital letter)",
|
||||||
|
"phrase-abbreviator_option_character_extraction": "字符提取",
|
||||||
|
"phrase-abbreviator_option_character_extraction_description": "Method for extracting characters from each word",
|
||||||
|
"phrase-abbreviator_option_character_extraction_choice_single": "仅首字母",
|
||||||
|
"phrase-abbreviator_option_character_extraction_choice_dual": "首字母 + 下一个元音/辅音",
|
||||||
|
"phrase-abbreviator_option_language": "语言",
|
||||||
|
"phrase-abbreviator_option_language_description": "Language for number word conversion",
|
||||||
|
"phrase-abbreviator_option_language_choice_en": "English",
|
||||||
|
"phrase-abbreviator_option_language_choice_de": "德语",
|
||||||
|
"phrase-abbreviator_option_language_choice_de_CH": "瑞士德语",
|
||||||
|
"phrase-abbreviator_option_language_choice_fr": "法语",
|
||||||
|
"phrase-abbreviator_option_language_choice_it": "意大利语",
|
||||||
|
"phrase-abbreviator_option_language_choice_es": "西班牙语",
|
||||||
|
"phrase-abbreviator_option_language_choice_pt": "葡萄牙语",
|
||||||
|
"phrase-abbreviator_option_language_choice_ja": "日语",
|
||||||
|
"phrase-abbreviator_option_language_choice_zh": "中文",
|
||||||
|
"phrase-abbreviator_option_add_number": "添加数字",
|
||||||
|
"phrase-abbreviator_option_add_number_description": "向密码中添加随机数字",
|
||||||
|
"phrase-abbreviator_option_number_digits": "数字数量",
|
||||||
|
"phrase-abbreviator_option_number_digits_description": "添加多少个随机数字(1-4)",
|
||||||
|
"phrase-abbreviator_option_number_position": "数字位置",
|
||||||
|
"phrase-abbreviator_option_number_position_description": "将数字放在密码前面或后面",
|
||||||
|
"phrase-abbreviator_option_number_position_choice_prepend": "前面",
|
||||||
|
"phrase-abbreviator_option_number_position_choice_append": "后面",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_openai": "OpenAI",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_claude": "Claude",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_gemini": "Gemini",
|
||||||
|
"phrase-abbreviator_option_ai_provider_choice_perplexity": "Perplexity",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-3.5-turbo": "GPT-3.5 Turbo",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4": "GPT-4",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4-turbo": "GPT-4 Turbo",
|
||||||
|
"phrase-abbreviator_option_ai_model_choice_gpt-4o": "GPT-4 Omni",
|
||||||
|
"collapse_group_basic": "基础选项",
|
||||||
|
"collapse_group_advanced": "高级选项",
|
||||||
|
"collapse_group_ai": "AI 功能",
|
||||||
|
"collapse_group_expert": "专家选项",
|
||||||
|
"collapse_group_character_set": "字符集",
|
||||||
|
"copied": "已复制",
|
||||||
|
"pwgen_option_type_choice_custom": "使用长度设置",
|
||||||
|
"roulette": "轮盘",
|
||||||
|
"stop": "停止",
|
||||||
|
"roulette_interval": "间隔(秒)",
|
||||||
|
"roulette_slow_down": "增加间隔(更慢)",
|
||||||
|
"roulette_speed_up": "减少间隔(更快)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Logs Directory
|
||||||
|
|
||||||
|
This directory contains application logs.
|
||||||
|
|
||||||
|
Make sure this directory is writable by the web server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod 755 logs/
|
||||||
|
```
|
||||||
|
|
||||||
|
Log files will be created automatically when the application runs.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Application\ApplicationFactory;
|
||||||
|
use DI\ContainerBuilder;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Slim\Factory\AppFactory;
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
|
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
// Build DI container
|
||||||
|
$containerBuilder = new ContainerBuilder();
|
||||||
|
|
||||||
|
// Set up dependencies
|
||||||
|
$dependencies = require __DIR__ . '/../config/dependencies.php';
|
||||||
|
$dependencies($containerBuilder);
|
||||||
|
|
||||||
|
$container = $containerBuilder->build();
|
||||||
|
|
||||||
|
AppFactory::setContainer($container);
|
||||||
|
$app = AppFactory::create();
|
||||||
|
|
||||||
|
// Add middleware
|
||||||
|
$middleware = require __DIR__ . '/../config/middleware.php';
|
||||||
|
$middleware($app);
|
||||||
|
|
||||||
|
// Register routes
|
||||||
|
$routes = require __DIR__ . '/../config/routes.php';
|
||||||
|
$routes($app);
|
||||||
|
|
||||||
|
$app->run();
|
||||||
@@ -0,0 +1,480 @@
|
|||||||
|
{
|
||||||
|
"openapi": "3.0.3",
|
||||||
|
"info": {
|
||||||
|
"title": "Password Generator API",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "Public password generation API and locale metadata for the password generator web app."
|
||||||
|
},
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8080"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"/api/v1/health": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Health check",
|
||||||
|
"description": "Returns the health status of the API service.",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HealthSuccess"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Error response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/locales/{lang}": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Fetch locale data",
|
||||||
|
"description": "Returns the translation resource for a given locale code.",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/LocaleSuccess"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Error response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "lang",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"en",
|
||||||
|
"de",
|
||||||
|
"de_CH",
|
||||||
|
"fr",
|
||||||
|
"it",
|
||||||
|
"es",
|
||||||
|
"pt",
|
||||||
|
"ja",
|
||||||
|
"zh"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Locale code to fetch translation data for."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/passwords": {
|
||||||
|
"get": {
|
||||||
|
"summary": "List available password generation types",
|
||||||
|
"description": "Returns the supported password generation algorithms.",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"types": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Available password generation types"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"success",
|
||||||
|
"data",
|
||||||
|
"timestamp"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Error response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/passwords/generate": {
|
||||||
|
"post": {
|
||||||
|
"summary": "Generate one or more passwords",
|
||||||
|
"description": "Generates passwords using the chosen algorithm and options.",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/PasswordGenerationSuccess"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Error response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/PasswordGenerationRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/passwords/generators": {
|
||||||
|
"get": {
|
||||||
|
"summary": "API route",
|
||||||
|
"description": "Generated from the public Slim route registry.",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Error response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/passwords/wordlists": {
|
||||||
|
"get": {
|
||||||
|
"summary": "List available XKCD wordlists",
|
||||||
|
"description": "Returns available language wordlists for passphrase generation.",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/WordlistsSuccess"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Error response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"ErrorResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": false
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Request failed"
|
||||||
|
},
|
||||||
|
"code": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "ERROR"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"success",
|
||||||
|
"error",
|
||||||
|
"timestamp"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"PasswordGenerationRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "high-entropy"
|
||||||
|
},
|
||||||
|
"count": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 1
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PasswordGenerationSuccess": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"passwords": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entropy_bits": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 128
|
||||||
|
},
|
||||||
|
"security_assessment": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Strong \u2014 this password has good entropy and is suitable for most uses."
|
||||||
|
},
|
||||||
|
"raw_technical_details": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"entropy_bits": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 77.5
|
||||||
|
},
|
||||||
|
"generator": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "XKCD Pass"
|
||||||
|
},
|
||||||
|
"length": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 28
|
||||||
|
},
|
||||||
|
"security_level": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Strong"
|
||||||
|
},
|
||||||
|
"entropy_breakdown": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"entropy_model": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "xkcd-candidate-space-v1"
|
||||||
|
},
|
||||||
|
"model_confidence": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"high",
|
||||||
|
"medium",
|
||||||
|
"low"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"entropy_bits": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 77.5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Generated 1 password(s) of type 'high-entropy'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"success",
|
||||||
|
"data",
|
||||||
|
"timestamp"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"PasswordTypesSuccess": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"types": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"success",
|
||||||
|
"data",
|
||||||
|
"timestamp"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"WordlistsSuccess": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"wordlists": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"count": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Available wordlists for XKCD password generation"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"success",
|
||||||
|
"data",
|
||||||
|
"timestamp"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"HealthSuccess": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "healthy"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "2.0.0"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"version",
|
||||||
|
"timestamp"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"LocaleSuccess": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Api;
|
||||||
|
|
||||||
|
use Slim\Factory\AppFactory;
|
||||||
|
|
||||||
|
final class OpenApiGenerator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Generate the public OpenAPI document from the registered Slim routes.
|
||||||
|
*/
|
||||||
|
public static function generate(string $baseUrl): array
|
||||||
|
{
|
||||||
|
$baseUrl = rtrim($baseUrl, '/');
|
||||||
|
|
||||||
|
$paths = [];
|
||||||
|
foreach (self::registeredPublicRoutes() as $routePath => $methods) {
|
||||||
|
$normalizedPath = self::normalizeRoutePattern($routePath);
|
||||||
|
$paths[$normalizedPath] = self::buildPathOperations($normalizedPath, $methods);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'openapi' => '3.0.3',
|
||||||
|
'info' => [
|
||||||
|
'title' => $_ENV['APP_NAME'] ?? 'Password Generator API',
|
||||||
|
'version' => $_ENV['APP_VERSION'] ?? '2.0.0',
|
||||||
|
'description' => 'Public password generation API and locale metadata for the password generator web app.',
|
||||||
|
],
|
||||||
|
'servers' => [
|
||||||
|
['url' => $baseUrl],
|
||||||
|
],
|
||||||
|
'paths' => $paths,
|
||||||
|
'components' => [
|
||||||
|
'schemas' => [
|
||||||
|
'ErrorResponse' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'success' => ['type' => 'boolean', 'example' => false],
|
||||||
|
'error' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'message' => ['type' => 'string', 'example' => 'Request failed'],
|
||||||
|
'code' => ['type' => 'string', 'example' => 'ERROR'],
|
||||||
|
'status' => ['type' => 'integer', 'example' => 400],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'timestamp' => ['type' => 'string', 'format' => 'date-time'],
|
||||||
|
],
|
||||||
|
'required' => ['success', 'error', 'timestamp'],
|
||||||
|
],
|
||||||
|
'PasswordGenerationRequest' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'type' => ['type' => 'string', 'example' => 'high-entropy'],
|
||||||
|
'count' => ['type' => 'integer', 'example' => 1],
|
||||||
|
'options' => ['type' => 'object', 'additionalProperties' => true],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'PasswordGenerationSuccess' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'success' => ['type' => 'boolean', 'example' => true],
|
||||||
|
'data' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'passwords' => [
|
||||||
|
'type' => 'array',
|
||||||
|
'items' => ['type' => 'string'],
|
||||||
|
],
|
||||||
|
'entropy_bits' => ['type' => 'number', 'format' => 'float', 'example' => 128],
|
||||||
|
'security_assessment' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'example' => 'Strong — this password has good entropy and is suitable for most uses.',
|
||||||
|
],
|
||||||
|
'raw_technical_details' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'entropy_bits' => ['type' => 'number', 'format' => 'float', 'example' => 77.5],
|
||||||
|
'generator' => ['type' => 'string', 'example' => 'XKCD Pass'],
|
||||||
|
'length' => ['type' => 'integer', 'example' => 28],
|
||||||
|
'security_level' => ['type' => 'string', 'example' => 'Strong'],
|
||||||
|
'entropy_breakdown' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'entropy_model' => ['type' => 'string', 'example' => 'xkcd-candidate-space-v1'],
|
||||||
|
'model_confidence' => ['type' => 'string', 'enum' => ['high', 'medium', 'low']],
|
||||||
|
'entropy_bits' => ['type' => 'number', 'format' => 'float', 'example' => 77.5],
|
||||||
|
],
|
||||||
|
'additionalProperties' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'additionalProperties' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'timestamp' => ['type' => 'string', 'format' => 'date-time'],
|
||||||
|
'message' => ['type' => 'string', 'example' => 'Generated 1 password(s) of type \'high-entropy\''],
|
||||||
|
],
|
||||||
|
'required' => ['success', 'data', 'timestamp'],
|
||||||
|
],
|
||||||
|
'PasswordTypesSuccess' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'success' => ['type' => 'boolean', 'example' => true],
|
||||||
|
'data' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'types' => [
|
||||||
|
'type' => 'array',
|
||||||
|
'items' => ['type' => 'string'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'timestamp' => ['type' => 'string', 'format' => 'date-time'],
|
||||||
|
],
|
||||||
|
'required' => ['success', 'data', 'timestamp'],
|
||||||
|
],
|
||||||
|
'WordlistsSuccess' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'success' => ['type' => 'boolean', 'example' => true],
|
||||||
|
'data' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'wordlists' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'additionalProperties' => true,
|
||||||
|
],
|
||||||
|
'count' => ['type' => 'integer', 'example' => 2],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'timestamp' => ['type' => 'string', 'format' => 'date-time'],
|
||||||
|
'message' => ['type' => 'string', 'example' => 'Available wordlists for XKCD password generation'],
|
||||||
|
],
|
||||||
|
'required' => ['success', 'data', 'timestamp'],
|
||||||
|
],
|
||||||
|
'HealthSuccess' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'status' => ['type' => 'string', 'example' => 'healthy'],
|
||||||
|
'version' => ['type' => 'string', 'example' => '2.0.0'],
|
||||||
|
'timestamp' => ['type' => 'string', 'format' => 'date-time'],
|
||||||
|
],
|
||||||
|
'required' => ['status', 'version', 'timestamp'],
|
||||||
|
],
|
||||||
|
'LocaleSuccess' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'additionalProperties' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function write(string $baseUrl, string $destination): void
|
||||||
|
{
|
||||||
|
$directory = dirname($destination);
|
||||||
|
if (!is_dir($directory)) {
|
||||||
|
mkdir($directory, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = json_encode(self::generate($baseUrl), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||||
|
if ($json === false) {
|
||||||
|
throw new \RuntimeException('Unable to encode OpenAPI document as JSON.');
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($destination, $json . PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect the public API routes as registered by config/routes.php.
|
||||||
|
*
|
||||||
|
* @return array<string, array<int, string>>
|
||||||
|
*/
|
||||||
|
private static function registeredPublicRoutes(): array
|
||||||
|
{
|
||||||
|
AppFactory::setContainer(new \DI\Container());
|
||||||
|
$app = AppFactory::create();
|
||||||
|
|
||||||
|
$routesDefinition = require __DIR__ . '/../../config/routes.php';
|
||||||
|
$routesDefinition($app);
|
||||||
|
|
||||||
|
$routes = [];
|
||||||
|
foreach ($app->getRouteCollector()->getRoutes() as $route) {
|
||||||
|
$pattern = $route->getPattern();
|
||||||
|
|
||||||
|
if (!str_starts_with($pattern, '/api/v1')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$methods = array_map('strtoupper', $route->getMethods());
|
||||||
|
$routes[$pattern] = array_values(array_unique($methods));
|
||||||
|
}
|
||||||
|
|
||||||
|
ksort($routes);
|
||||||
|
|
||||||
|
return $routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function normalizeRoutePattern(string $pattern): string
|
||||||
|
{
|
||||||
|
return preg_replace('/\{([a-zA-Z0-9_]+):.*\}/', '{$1}', $pattern) ?? $pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $methods
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private static function buildPathOperations(string $routePath, array $methods): array
|
||||||
|
{
|
||||||
|
$operations = [];
|
||||||
|
$metadata = self::routeMetadata($routePath);
|
||||||
|
|
||||||
|
foreach ($methods as $method) {
|
||||||
|
$methodName = strtolower($method);
|
||||||
|
$operations[$methodName] = [
|
||||||
|
'summary' => $metadata['summary'],
|
||||||
|
'description' => $metadata['description'],
|
||||||
|
'responses' => self::responses($routePath, $metadata['successCode'], $metadata['errorCode']),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($method === 'POST' && $routePath === '/api/v1/passwords/generate') {
|
||||||
|
$operations[$methodName]['requestBody'] = [
|
||||||
|
'required' => true,
|
||||||
|
'content' => [
|
||||||
|
'application/json' => [
|
||||||
|
'schema' => [
|
||||||
|
'$ref' => '#/components/schemas/PasswordGenerationRequest',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($routePath, '{lang}')) {
|
||||||
|
$operations[$methodName]['parameters'] = [[
|
||||||
|
'name' => 'lang',
|
||||||
|
'in' => 'path',
|
||||||
|
'required' => true,
|
||||||
|
'schema' => ['type' => 'string', 'enum' => ['en', 'de', 'de_CH', 'fr', 'it', 'es', 'pt', 'ja', 'zh']],
|
||||||
|
'description' => 'Locale code to fetch translation data for.',
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $operations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{summary:string,description:string,successCode:int,errorCode:int}
|
||||||
|
*/
|
||||||
|
private static function routeMetadata(string $routePath): array
|
||||||
|
{
|
||||||
|
$metadata = [
|
||||||
|
'/api/v1/passwords' => [
|
||||||
|
'summary' => 'List available password generation types',
|
||||||
|
'description' => 'Returns the supported password generation algorithms.',
|
||||||
|
'successCode' => 200,
|
||||||
|
'errorCode' => 500,
|
||||||
|
],
|
||||||
|
'/api/v1/passwords/generate' => [
|
||||||
|
'summary' => 'Generate one or more passwords',
|
||||||
|
'description' => 'Generates passwords using the chosen algorithm and options.',
|
||||||
|
'successCode' => 200,
|
||||||
|
'errorCode' => 400,
|
||||||
|
],
|
||||||
|
'/api/v1/passwords/wordlists' => [
|
||||||
|
'summary' => 'List available XKCD wordlists',
|
||||||
|
'description' => 'Returns available language wordlists for passphrase generation.',
|
||||||
|
'successCode' => 200,
|
||||||
|
'errorCode' => 500,
|
||||||
|
],
|
||||||
|
'/api/v1/health' => [
|
||||||
|
'summary' => 'Health check',
|
||||||
|
'description' => 'Returns the health status of the API service.',
|
||||||
|
'successCode' => 200,
|
||||||
|
'errorCode' => 500,
|
||||||
|
],
|
||||||
|
'/api/v1/locales/{lang}' => [
|
||||||
|
'summary' => 'Fetch locale data',
|
||||||
|
'description' => 'Returns the translation resource for a given locale code.',
|
||||||
|
'successCode' => 200,
|
||||||
|
'errorCode' => 404,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
return $metadata[$routePath] ?? [
|
||||||
|
'summary' => 'API route',
|
||||||
|
'description' => 'Generated from the public Slim route registry.',
|
||||||
|
'successCode' => 200,
|
||||||
|
'errorCode' => 500,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private static function responses(string $routePath, int $successCode, int $errorCode): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
(string) $successCode => [
|
||||||
|
'description' => 'Successful response',
|
||||||
|
'content' => [
|
||||||
|
'application/json' => [
|
||||||
|
'schema' => self::successSchemaFor($routePath),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
(string) $errorCode => [
|
||||||
|
'description' => 'Error response',
|
||||||
|
'content' => [
|
||||||
|
'application/json' => [
|
||||||
|
'schema' => [
|
||||||
|
'$ref' => '#/components/schemas/ErrorResponse',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function successSchemaFor(string $routePath): array
|
||||||
|
{
|
||||||
|
return match ($routePath) {
|
||||||
|
'/api/v1/passwords' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'success' => ['type' => 'boolean', 'example' => true],
|
||||||
|
'data' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'types' => ['type' => 'array', 'items' => ['type' => 'string']],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'timestamp' => ['type' => 'string', 'format' => 'date-time'],
|
||||||
|
'message' => ['type' => 'string', 'example' => 'Available password generation types'],
|
||||||
|
],
|
||||||
|
'required' => ['success', 'data', 'timestamp'],
|
||||||
|
],
|
||||||
|
'/api/v1/passwords/generate' => [
|
||||||
|
'$ref' => '#/components/schemas/PasswordGenerationSuccess',
|
||||||
|
],
|
||||||
|
'/api/v1/passwords/wordlists' => [
|
||||||
|
'$ref' => '#/components/schemas/WordlistsSuccess',
|
||||||
|
],
|
||||||
|
'/api/v1/health' => [
|
||||||
|
'$ref' => '#/components/schemas/HealthSuccess',
|
||||||
|
],
|
||||||
|
'/api/v1/locales/{lang}' => [
|
||||||
|
'$ref' => '#/components/schemas/LocaleSuccess',
|
||||||
|
],
|
||||||
|
default => [
|
||||||
|
'type' => 'object',
|
||||||
|
'additionalProperties' => true,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Api;
|
||||||
|
|
||||||
|
use App\Managers\PasswordGeneratorManager;
|
||||||
|
use App\Utils\ConfigLoader;
|
||||||
|
use App\Utils\ResponseHelper;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
class PasswordController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private PasswordGeneratorManager $generatorManager,
|
||||||
|
private ResponseHelper $responseHelper,
|
||||||
|
private LoggerInterface $logger
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getPasswordTypes(Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$types = $this->generatorManager->getAvailableTypes();
|
||||||
|
|
||||||
|
$this->logger->info('Password types requested', ['count' => count($types)]);
|
||||||
|
|
||||||
|
return $this->responseHelper->successResponse(
|
||||||
|
$response,
|
||||||
|
['types' => $types],
|
||||||
|
'Available password generation types'
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->logger->error('Error fetching password types', ['error' => $e->getMessage()]);
|
||||||
|
|
||||||
|
return $this->responseHelper->errorResponse(
|
||||||
|
$response,
|
||||||
|
'Failed to fetch password types',
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGenerators(Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$definitions = array_values($this->generatorManager->getGeneratorDefinitions());
|
||||||
|
$appConfig = ConfigLoader::load();
|
||||||
|
$rouletteIntervalSeconds = (int)($appConfig['generator']['roulette_interval_seconds'] ?? 3);
|
||||||
|
$rouletteIntervalSeconds = max(1, min(30, $rouletteIntervalSeconds));
|
||||||
|
|
||||||
|
$this->logger->info('Generator definitions requested', ['count' => count($definitions)]);
|
||||||
|
|
||||||
|
return $this->responseHelper->successResponse(
|
||||||
|
$response,
|
||||||
|
[
|
||||||
|
'generators' => $definitions,
|
||||||
|
'settings' => [
|
||||||
|
'roulette_interval_seconds' => $rouletteIntervalSeconds,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'Available password generators'
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->logger->error('Error fetching generator definitions', ['error' => $e->getMessage()]);
|
||||||
|
|
||||||
|
return $this->responseHelper->errorResponse(
|
||||||
|
$response,
|
||||||
|
'Failed to fetch generator definitions',
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NEW: Get available wordlists for XKCD generator
|
||||||
|
*/
|
||||||
|
public function getAvailableWordlists(Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$xkcdGenerator = $this->generatorManager->getGenerator('xkcdpass');
|
||||||
|
$availableLanguages = $xkcdGenerator->getAvailableLanguages();
|
||||||
|
|
||||||
|
// Filter to only show available wordlists
|
||||||
|
$availableWordlists = array_filter($availableLanguages, function($lang) {
|
||||||
|
return $lang['available'];
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no wordlists are available, provide default language metadata
|
||||||
|
if (empty($availableWordlists)) {
|
||||||
|
$this->logger->warning('No wordlist files found, using default language metadata');
|
||||||
|
|
||||||
|
// Provide basic language info even without files
|
||||||
|
$defaultLanguages = [
|
||||||
|
'en' => [
|
||||||
|
'name' => 'English (Default)',
|
||||||
|
'description' => 'Built-in English word list',
|
||||||
|
'file' => 'eff_large_wordlist_en.txt',
|
||||||
|
'available' => false,
|
||||||
|
'fallback' => true
|
||||||
|
],
|
||||||
|
'de' => [
|
||||||
|
'name' => 'German (Default)',
|
||||||
|
'description' => 'Built-in German word list',
|
||||||
|
'file' => 'german_common_words_de.txt',
|
||||||
|
'available' => false,
|
||||||
|
'fallback' => true
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$availableWordlists = $defaultLanguages;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->info('Wordlists requested', [
|
||||||
|
'available_count' => count($availableWordlists),
|
||||||
|
'languages' => array_keys($availableWordlists)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->responseHelper->successResponse(
|
||||||
|
$response,
|
||||||
|
[
|
||||||
|
'wordlists' => $availableWordlists,
|
||||||
|
'count' => count($availableWordlists)
|
||||||
|
],
|
||||||
|
'Available wordlists for XKCD password generation'
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->logger->error('Error fetching wordlists', ['error' => $e->getMessage()]);
|
||||||
|
|
||||||
|
return $this->responseHelper->errorResponse(
|
||||||
|
$response,
|
||||||
|
'Failed to fetch available wordlists',
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generatePasswords(Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$data = $request->getParsedBody();
|
||||||
|
|
||||||
|
$type = $data['type'] ?? 'high-entropy';
|
||||||
|
$count = max(1, min(50, (int)($data['count'] ?? 1)));
|
||||||
|
$options = $data['options'] ?? [];
|
||||||
|
|
||||||
|
// Validate required parameters
|
||||||
|
if (empty($type)) {
|
||||||
|
return $this->responseHelper->errorResponse(
|
||||||
|
$response,
|
||||||
|
'Password type is required',
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->generatorManager->generate($type, $count, $options);
|
||||||
|
|
||||||
|
$this->logger->info('Passwords generated', [
|
||||||
|
'type' => $type,
|
||||||
|
'count' => $count,
|
||||||
|
'entropy' => $result['entropy_bits'],
|
||||||
|
'ip' => $this->getClientIp($request)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->responseHelper->successResponse(
|
||||||
|
$response,
|
||||||
|
$result,
|
||||||
|
"Generated {$count} password(s) of type '{$type}'"
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (\InvalidArgumentException $e) {
|
||||||
|
$this->logger->warning('Invalid password generation request', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'ip' => $this->getClientIp($request)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->responseHelper->errorResponse(
|
||||||
|
$response,
|
||||||
|
$e->getMessage(),
|
||||||
|
400
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->logger->error('Error generating passwords', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'ip' => $this->getClientIp($request)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->responseHelper->errorResponse(
|
||||||
|
$response,
|
||||||
|
'Failed to generate passwords',
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getClientIp(Request $request): string
|
||||||
|
{
|
||||||
|
$serverParams = $request->getServerParams();
|
||||||
|
|
||||||
|
if (!empty($serverParams['HTTP_CLIENT_IP'])) {
|
||||||
|
return $serverParams['HTTP_CLIENT_IP'];
|
||||||
|
} elseif (!empty($serverParams['HTTP_X_FORWARDED_FOR'])) {
|
||||||
|
return explode(',', $serverParams['HTTP_X_FORWARDED_FOR'])[0];
|
||||||
|
} elseif (!empty($serverParams['REMOTE_ADDR'])) {
|
||||||
|
return $serverParams['REMOTE_ADDR'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '127.0.0.1';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,469 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Generators;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use RuntimeException;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use App\Utils\ConfigLoader;
|
||||||
|
use App\Services\Logging\LoggingHelper;
|
||||||
|
use PHPUnit\TextUI\XmlConfiguration\Logging\Logging;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base class for password generators
|
||||||
|
* Implements common functionality including i18n key derivation and validation
|
||||||
|
*
|
||||||
|
* @package App\Generators
|
||||||
|
*/
|
||||||
|
abstract class AbstractPasswordGenerator implements PasswordGeneratorInterface
|
||||||
|
{
|
||||||
|
protected ?LoggerInterface $logger = null;
|
||||||
|
protected array $config = [];
|
||||||
|
|
||||||
|
public function __construct(?LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
$this->logger = $logger;
|
||||||
|
$this->loadConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load configuration from config/config.json.
|
||||||
|
*/
|
||||||
|
protected function loadConfiguration(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->config = ConfigLoader::load();
|
||||||
|
LoggingHelper::debug($this->logger, $this->getId(), 'Configuration loaded');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
LoggingHelper::warning(
|
||||||
|
$this->logger,
|
||||||
|
$this->getId(),
|
||||||
|
'Failed to load config',
|
||||||
|
['error' => $e->getMessage()]
|
||||||
|
);
|
||||||
|
$this->config = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get configuration value using dot notation with safe nested access
|
||||||
|
*
|
||||||
|
* Provides a convenient way to access nested configuration arrays using
|
||||||
|
* dot notation (e.g., 'llm.providers.openai.display_name'). Returns a
|
||||||
|
* default value if the path doesn't exist, preventing errors from missing keys.
|
||||||
|
*
|
||||||
|
* This is particularly useful for:
|
||||||
|
* - LLM provider configuration: $this->getConfigValue('llm.providers.openai')
|
||||||
|
* - Locale settings: $this->getConfigValue('locale.default', 'en')
|
||||||
|
* - Logging levels: $this->getConfigValue('logging.level', 'info')
|
||||||
|
* - Any nested configuration structure
|
||||||
|
*
|
||||||
|
* @param string $path Configuration path using dot notation (e.g., 'key1.key2.key3')
|
||||||
|
* @param mixed $default Fallback value if path doesn't exist (default: null)
|
||||||
|
* @return mixed The configuration value at the path, or $default if not found
|
||||||
|
*
|
||||||
|
* @throws RuntimeException If $this->config is not loaded or initialized
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Get a configuration value with fallback
|
||||||
|
* $lang = $this->getConfigValue('locale.default', 'en');
|
||||||
|
*
|
||||||
|
* // Get nested LLM configuration
|
||||||
|
* $provider = $this->getConfigValue('llm.providers.openai', null);
|
||||||
|
*
|
||||||
|
* // Get logging level (defaults to 'info' if not set)
|
||||||
|
* $level = $this->getConfigValue('logging.level', 'info');
|
||||||
|
*
|
||||||
|
* // Get a boolean value safely
|
||||||
|
* $debugMode = $this->getConfigValue('app.debug', false);
|
||||||
|
*
|
||||||
|
* // Navigate deep into configuration
|
||||||
|
* $displayName = $this->getConfigValue('llm.providers.openai.display_name', 'OpenAI');
|
||||||
|
*/
|
||||||
|
protected function getConfigValue(string $path, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
// Guard: Ensure config is loaded
|
||||||
|
if ($this->config === null) {
|
||||||
|
if ($this->logger) {
|
||||||
|
$this->logger->warning(
|
||||||
|
$this->getId() . ': getConfigValue called but config not loaded',
|
||||||
|
['requested_path' => $path]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle empty path
|
||||||
|
if (empty($path)) {
|
||||||
|
return $this->config;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split path by dots to get individual keys
|
||||||
|
$keys = explode('.', $path);
|
||||||
|
|
||||||
|
// Start with the full config array
|
||||||
|
$current = $this->config;
|
||||||
|
|
||||||
|
// Traverse through each key in the path
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
// Validate key is not empty
|
||||||
|
if (empty($key)) {
|
||||||
|
LoggingHelper::warning(
|
||||||
|
$this->logger,
|
||||||
|
$this->getId(),
|
||||||
|
'getConfigValue received empty key in path',
|
||||||
|
['path' => $path]
|
||||||
|
);
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if current value is an array and has the key
|
||||||
|
if (is_array($current)) {
|
||||||
|
if (isset($current[$key])) {
|
||||||
|
// Move to the next level
|
||||||
|
$current = $current[$key];
|
||||||
|
} else {
|
||||||
|
// Key not found in this level
|
||||||
|
LoggingHelper::debug(
|
||||||
|
$this->logger,
|
||||||
|
$this->getId(),
|
||||||
|
'getConfigValue key not found in path',
|
||||||
|
['key' => $key, 'path' => $path]
|
||||||
|
);
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Current value is not an array, can't navigate further
|
||||||
|
LoggingHelper::debug(
|
||||||
|
$this->logger,
|
||||||
|
$this->getId(),
|
||||||
|
'getConfigValue cannot navigate non-array value',
|
||||||
|
['key' => $key, 'path' => $path]
|
||||||
|
);
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Successfully retrieved the value at the path
|
||||||
|
LoggingHelper::debug(
|
||||||
|
$this->logger,
|
||||||
|
$this->getId(),
|
||||||
|
'getConfigValue retrieved value',
|
||||||
|
['path' => $path, 'type' => gettype($current)]
|
||||||
|
);
|
||||||
|
|
||||||
|
return $current;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get generator name i18n key (programmatically derived)
|
||||||
|
*/
|
||||||
|
public function getNameKey(): string
|
||||||
|
{
|
||||||
|
return $this->getId() . '_name';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get generator description i18n key (programmatically derived)
|
||||||
|
*/
|
||||||
|
public function getDescriptionKey(): string
|
||||||
|
{
|
||||||
|
return $this->getId() . '_description';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get i18n key for option label (programmatically derived)
|
||||||
|
*/
|
||||||
|
public function getOptionLabelKey(string $optionName): string
|
||||||
|
{
|
||||||
|
return $this->getId() . '_option_' . $optionName . '_label';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get i18n key for option description (programmatically derived)
|
||||||
|
*/
|
||||||
|
public function getOptionDescriptionKey(string $optionName): string
|
||||||
|
{
|
||||||
|
return $this->getId() . '_option_' . $optionName . '_description';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get i18n key for option group title (programmatically derived)
|
||||||
|
*/
|
||||||
|
public function getOptionGroupTitleKey(string $groupName): string
|
||||||
|
{
|
||||||
|
return $this->getId() . '_options_group_' . $groupName . '_title';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get i18n key for select option choice (programmatically derived)
|
||||||
|
*/
|
||||||
|
public function getOptionChoiceLabelKey(string $optionName, string $choiceValue): string
|
||||||
|
{
|
||||||
|
return $this->getId() . '_option_' . $optionName . '_choice_' . $choiceValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get i18n key for validation error (programmatically derived)
|
||||||
|
*/
|
||||||
|
public function getValidationErrorKey(string $optionName): string
|
||||||
|
{
|
||||||
|
return $this->getId() . '_option_' . $optionName . '_validation_error';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all i18n keys used by this generator
|
||||||
|
*
|
||||||
|
* Automatically collects all keys from generator metadata and options
|
||||||
|
*/
|
||||||
|
public function getAllI18nKeys(): array
|
||||||
|
{
|
||||||
|
$keys = [];
|
||||||
|
|
||||||
|
// Generator metadata keys
|
||||||
|
$keys[] = $this->getNameKey();
|
||||||
|
$keys[] = $this->getDescriptionKey();
|
||||||
|
|
||||||
|
$optionDefs = $this->getOptionDefinitions();
|
||||||
|
$groups = [];
|
||||||
|
|
||||||
|
foreach ($optionDefs as $optionName => $definition) {
|
||||||
|
// Option label and description keys
|
||||||
|
$keys[] = $this->getOptionLabelKey($optionName);
|
||||||
|
$keys[] = $this->getOptionDescriptionKey($optionName);
|
||||||
|
$keys[] = $this->getValidationErrorKey($optionName);
|
||||||
|
|
||||||
|
// Group title key (collect unique groups)
|
||||||
|
$group = $definition['group'] ?? 'basic';
|
||||||
|
if (!in_array($group, $groups)) {
|
||||||
|
$groups[] = $group;
|
||||||
|
$keys[] = $this->getOptionGroupTitleKey($group);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select option choice keys (if translateChoices is true)
|
||||||
|
if ($definition['type'] === 'select' && ($definition['translateChoices'] ?? true)) {
|
||||||
|
$choices = $definition['choices'] ?? [];
|
||||||
|
foreach ($choices as $choice) {
|
||||||
|
$value = is_array($choice) ? $choice['value'] : $choice;
|
||||||
|
$keys[] = $this->getOptionChoiceLabelKey($optionName, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default options from definitions
|
||||||
|
*/
|
||||||
|
public function getDefaultOptions(): array
|
||||||
|
{
|
||||||
|
$defaults = [];
|
||||||
|
foreach ($this->getOptionDefinitions() as $name => $definition) {
|
||||||
|
$defaults[$name] = $definition['default'] ?? null;
|
||||||
|
}
|
||||||
|
return $defaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get complete option schema with derived i18n keys
|
||||||
|
*/
|
||||||
|
public function getOptionSchema(): array
|
||||||
|
{
|
||||||
|
$schema = [];
|
||||||
|
$optionDefs = $this->getOptionDefinitions();
|
||||||
|
|
||||||
|
foreach ($optionDefs as $name => $definition) {
|
||||||
|
$option = [
|
||||||
|
'name' => $name,
|
||||||
|
'type' => $definition['type'],
|
||||||
|
'labelKey' => $this->getOptionLabelKey($name),
|
||||||
|
'descriptionKey' => $this->getOptionDescriptionKey($name),
|
||||||
|
'default' => $definition['default'] ?? null,
|
||||||
|
'required' => $definition['required'] ?? false,
|
||||||
|
'group' => $definition['group'] ?? 'basic',
|
||||||
|
'groupTitleKey' => $this->getOptionGroupTitleKey($definition['group'] ?? 'basic'),
|
||||||
|
'validationErrorKey' => $this->getValidationErrorKey($name)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Type-specific properties
|
||||||
|
switch ($definition['type']) {
|
||||||
|
case 'integer':
|
||||||
|
if (isset($definition['min'])) $option['min'] = $definition['min'];
|
||||||
|
if (isset($definition['max'])) $option['max'] = $definition['max'];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'string':
|
||||||
|
if (isset($definition['minLength'])) $option['minLength'] = $definition['minLength'];
|
||||||
|
if (isset($definition['maxLength'])) $option['maxLength'] = $definition['maxLength'];
|
||||||
|
if (isset($definition['pattern'])) $option['pattern'] = $definition['pattern'];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'select':
|
||||||
|
$translateChoices = $definition['translateChoices'] ?? true;
|
||||||
|
$option['translateChoices'] = $translateChoices;
|
||||||
|
|
||||||
|
$formattedChoices = [];
|
||||||
|
foreach ($definition['choices'] ?? [] as $choice) {
|
||||||
|
if (is_array($choice)) {
|
||||||
|
$value = $choice['value'];
|
||||||
|
$label = $choice['label'];
|
||||||
|
} else {
|
||||||
|
$value = $choice;
|
||||||
|
$label = $choice;
|
||||||
|
}
|
||||||
|
|
||||||
|
$formattedChoices[] = [
|
||||||
|
'value' => $value,
|
||||||
|
'label' => $label,
|
||||||
|
'labelKey' => $translateChoices
|
||||||
|
? $this->getOptionChoiceLabelKey($name, $value)
|
||||||
|
: null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$option['choices'] = $formattedChoices;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$schema[] = $option;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate options against definitions
|
||||||
|
*/
|
||||||
|
public function validateOptions(array $options): array
|
||||||
|
{
|
||||||
|
$definitions = $this->getOptionDefinitions();
|
||||||
|
$validated = $this->getDefaultOptions();
|
||||||
|
|
||||||
|
foreach ($definitions as $name => $definition) {
|
||||||
|
// Skip if option not provided and not required
|
||||||
|
if (!isset($options[$name])) {
|
||||||
|
if ($definition['required'] ?? false) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
"Required option '{$name}' is missing"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $options[$name];
|
||||||
|
|
||||||
|
// Type-specific validation
|
||||||
|
switch ($definition['type']) {
|
||||||
|
case 'integer':
|
||||||
|
if (!is_int($value) && !is_numeric($value)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
"Option '{$name}' must be an integer"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$value = (int)$value;
|
||||||
|
|
||||||
|
if (isset($definition['min']) && $value < $definition['min']) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
"Option '{$name}' must be at least {$definition['min']}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($definition['max']) && $value > $definition['max']) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
"Option '{$name}' must be at most {$definition['max']}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'boolean':
|
||||||
|
if (!is_bool($value) && !in_array($value, [0, 1, '0', '1'], true)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
"Option '{$name}' must be a boolean"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$value = (bool)$value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'string':
|
||||||
|
if (!is_string($value)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
"Option '{$name}' must be a string"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$length = strlen($value);
|
||||||
|
if (isset($definition['minLength']) && $length < $definition['minLength']) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
"Option '{$name}' must be at least {$definition['minLength']} characters"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($definition['maxLength']) && $length > $definition['maxLength']) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
"Option '{$name}' must be at most {$definition['maxLength']} characters"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($definition['pattern']) && !preg_match($definition['pattern'], $value)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
"Option '{$name}' does not match required pattern"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'select':
|
||||||
|
if (!is_string($value)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
"Option '{$name}' must be a string"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validValues = array_map(function ($choice) {
|
||||||
|
return is_array($choice) ? $choice['value'] : $choice;
|
||||||
|
}, $definition['choices'] ?? []);
|
||||||
|
|
||||||
|
if (!in_array($value, $validValues, true)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
"Option '{$name}' must be one of: " . implode(', ', $validValues)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated[$name] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $validated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse wordlist entry that may contain word and explanation separated by tab
|
||||||
|
* Returns array with 'word' and 'explanation' keys
|
||||||
|
*/
|
||||||
|
protected function parseWordlistEntry(string $entry): array
|
||||||
|
{
|
||||||
|
$parts = explode("\t", $entry, 2);
|
||||||
|
return [
|
||||||
|
'word' => trim($parts[0]),
|
||||||
|
'explanation' => isset($parts[1]) ? trim($parts[1]) : null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format multiple word explanations for display
|
||||||
|
*/
|
||||||
|
protected function formatExplanations(array $wordExplanations): ?string
|
||||||
|
{
|
||||||
|
$explanations = array_filter(
|
||||||
|
array_column($wordExplanations, 'explanation')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (empty($explanations)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' — ', $explanations);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PHP fallback implementation for pwgen logic (pronounceable passwords).
|
||||||
|
*
|
||||||
|
* Based on NIST FIPS 181 "Automated Password Generator" standard
|
||||||
|
* and inspired by roderik/pwgen-php (https://github.com/roderik/pwgen-php)
|
||||||
|
*
|
||||||
|
* Original pwgen concept:
|
||||||
|
* Copyright (c) 2001 Theodore Ts'o <tytso@alum.mit.edu>
|
||||||
|
*
|
||||||
|
* PHP implementation credits:
|
||||||
|
* Copyright (c) 2012 Roderik van der Veer
|
||||||
|
* BSD License (see https://github.com/roderik/pwgen-php/blob/master/LICENSE)
|
||||||
|
* Original source: https://github.com/roderik/pwgen-php
|
||||||
|
*
|
||||||
|
* Modernized for PHP 8+ by Password Generator Project, 2025.
|
||||||
|
* This class provides a fallback for pwgen-style password generation when
|
||||||
|
* the native pwgen executable is not available.
|
||||||
|
*
|
||||||
|
* Based on NIST FIPS 181 - Federal Information Processing Standards Publication 181
|
||||||
|
* "Automated Password Generator" (1993)
|
||||||
|
*/
|
||||||
|
class PwGenPhpNg
|
||||||
|
{
|
||||||
|
// Character classification flags (based on NIST FIPS 181)
|
||||||
|
private const VOWEL = 0x01;
|
||||||
|
private const CONSONANT = 0x02;
|
||||||
|
private const DIPHTHONG = 0x04;
|
||||||
|
private const NOT_FIRST = 0x08;
|
||||||
|
|
||||||
|
// Unit definitions with pronunciability rules
|
||||||
|
private array $elements = [
|
||||||
|
'a' => ['flags' => self::VOWEL, 'weight' => 4],
|
||||||
|
'ae' => ['flags' => self::VOWEL | self::DIPHTHONG, 'weight' => 1],
|
||||||
|
'ah' => ['flags' => self::VOWEL | self::DIPHTHONG, 'weight' => 1],
|
||||||
|
'ai' => ['flags' => self::VOWEL | self::DIPHTHONG, 'weight' => 1],
|
||||||
|
'b' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||||
|
'c' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||||
|
'ch' => ['flags' => self::CONSONANT | self::DIPHTHONG, 'weight' => 1],
|
||||||
|
'd' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||||
|
'e' => ['flags' => self::VOWEL, 'weight' => 4],
|
||||||
|
'ee' => ['flags' => self::VOWEL | self::DIPHTHONG, 'weight' => 1],
|
||||||
|
'ei' => ['flags' => self::VOWEL | self::DIPHTHONG, 'weight' => 1],
|
||||||
|
'f' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||||
|
'g' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||||
|
'gh' => ['flags' => self::CONSONANT | self::DIPHTHONG | self::NOT_FIRST, 'weight' => 1],
|
||||||
|
'h' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||||
|
'i' => ['flags' => self::VOWEL, 'weight' => 4],
|
||||||
|
'ie' => ['flags' => self::VOWEL | self::DIPHTHONG, 'weight' => 1],
|
||||||
|
'j' => ['flags' => self::CONSONANT, 'weight' => 1],
|
||||||
|
'k' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||||
|
'l' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||||
|
'm' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||||
|
'n' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||||
|
'ng' => ['flags' => self::CONSONANT | self::DIPHTHONG | self::NOT_FIRST, 'weight' => 1],
|
||||||
|
'o' => ['flags' => self::VOWEL, 'weight' => 4],
|
||||||
|
'oh' => ['flags' => self::VOWEL | self::DIPHTHONG, 'weight' => 1],
|
||||||
|
'oo' => ['flags' => self::VOWEL | self::DIPHTHONG, 'weight' => 1],
|
||||||
|
'p' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||||
|
'ph' => ['flags' => self::CONSONANT | self::DIPHTHONG, 'weight' => 1],
|
||||||
|
'qu' => ['flags' => self::CONSONANT | self::DIPHTHONG, 'weight' => 1],
|
||||||
|
'r' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||||
|
'rh' => ['flags' => self::CONSONANT | self::DIPHTHONG, 'weight' => 1],
|
||||||
|
's' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||||
|
'sh' => ['flags' => self::CONSONANT | self::DIPHTHONG, 'weight' => 1],
|
||||||
|
't' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||||
|
'th' => ['flags' => self::CONSONANT | self::DIPHTHONG, 'weight' => 1],
|
||||||
|
'u' => ['flags' => self::VOWEL, 'weight' => 4],
|
||||||
|
'v' => ['flags' => self::CONSONANT, 'weight' => 1],
|
||||||
|
'w' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||||
|
'wh' => ['flags' => self::CONSONANT | self::DIPHTHONG, 'weight' => 1],
|
||||||
|
'x' => ['flags' => self::CONSONANT | self::NOT_FIRST, 'weight' => 1],
|
||||||
|
'y' => ['flags' => self::VOWEL, 'weight' => 2],
|
||||||
|
'z' => ['flags' => self::CONSONANT, 'weight' => 1],
|
||||||
|
];
|
||||||
|
|
||||||
|
private array $vowels = [];
|
||||||
|
private array $consonants = [];
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->initializePhonemes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize phoneme arrays based on element classification
|
||||||
|
*/
|
||||||
|
private function initializePhonemes(): void
|
||||||
|
{
|
||||||
|
foreach ($this->elements as $phoneme => $properties) {
|
||||||
|
if ($properties['flags'] & self::VOWEL) {
|
||||||
|
for ($i = 0; $i < $properties['weight']; $i++) {
|
||||||
|
$this->vowels[] = $phoneme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($properties['flags'] & self::CONSONANT) {
|
||||||
|
for ($i = 0; $i < $properties['weight']; $i++) {
|
||||||
|
$this->consonants[] = $phoneme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate pronounceable passwords using pwgen logic
|
||||||
|
*
|
||||||
|
* @param int $length Minimum length of password
|
||||||
|
* @param int $count Number of passwords to generate
|
||||||
|
* @param bool $includeNumbers Include numbers in passwords
|
||||||
|
* @param bool $includeCapitals Include capital letters
|
||||||
|
* @return array Array of generated passwords
|
||||||
|
*/
|
||||||
|
public function generate(int $length = 8, int $count = 1, bool $includeNumbers = true, bool $includeCapitals = true): array
|
||||||
|
{
|
||||||
|
if ($length < 4) {
|
||||||
|
$length = 4; // Minimum for pronounceable passwords
|
||||||
|
}
|
||||||
|
|
||||||
|
$passwords = [];
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
$password = $this->generatePassword($length, $includeNumbers, $includeCapitals);
|
||||||
|
$passwords[] = $password;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $passwords;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a single pronounceable password
|
||||||
|
*/
|
||||||
|
private function generatePassword(int $length, bool $includeNumbers, bool $includeCapitals): string
|
||||||
|
{
|
||||||
|
$password = '';
|
||||||
|
$currentLength = 0;
|
||||||
|
$lastWasVowel = false;
|
||||||
|
$syllableLength = 0;
|
||||||
|
$maxAttempts = 100;
|
||||||
|
$attempts = 0;
|
||||||
|
|
||||||
|
while ($currentLength < $length && $attempts < $maxAttempts) {
|
||||||
|
$attempts++;
|
||||||
|
|
||||||
|
// Start new syllable with consonant, then alternate
|
||||||
|
if ($syllableLength === 0 || ($syllableLength > 0 && $lastWasVowel)) {
|
||||||
|
// Choose consonant
|
||||||
|
$phoneme = $this->selectConsonant($currentLength === 0);
|
||||||
|
$lastWasVowel = false;
|
||||||
|
} else {
|
||||||
|
// Choose vowel
|
||||||
|
$phoneme = $this->selectVowel();
|
||||||
|
$lastWasVowel = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if adding this phoneme exceeds length
|
||||||
|
if ($currentLength + strlen($phoneme) > $length) {
|
||||||
|
// Try to find a shorter phoneme or end the password
|
||||||
|
if ($lastWasVowel) {
|
||||||
|
$phoneme = $this->selectVowel(1); // Single character vowel
|
||||||
|
} else {
|
||||||
|
$phoneme = $this->selectConsonant(false, 1); // Single character consonant
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($currentLength + strlen($phoneme) > $length) {
|
||||||
|
break; // Can't fit anything more
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$password .= $phoneme;
|
||||||
|
$currentLength += strlen($phoneme);
|
||||||
|
$syllableLength++;
|
||||||
|
|
||||||
|
// End syllable after 2-4 phonemes
|
||||||
|
if ($syllableLength >= 2 && random_int(0, 2) === 0) {
|
||||||
|
$syllableLength = 0;
|
||||||
|
$lastWasVowel = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply post-processing
|
||||||
|
if ($includeCapitals) {
|
||||||
|
$password = $this->addCapitalization($password);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($includeNumbers && strlen($password) < $length) {
|
||||||
|
$password = $this->addNumbers($password, $length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $password;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a consonant phoneme
|
||||||
|
*/
|
||||||
|
private function selectConsonant(bool $isFirst = false, int $maxLength = PHP_INT_MAX): string
|
||||||
|
{
|
||||||
|
$candidates = [];
|
||||||
|
|
||||||
|
foreach ($this->consonants as $consonant) {
|
||||||
|
$element = $this->elements[$consonant];
|
||||||
|
|
||||||
|
// Skip if not allowed at beginning
|
||||||
|
if ($isFirst && ($element['flags'] & self::NOT_FIRST)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if too long
|
||||||
|
if (strlen($consonant) > $maxLength) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidates[] = $consonant;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($candidates)) {
|
||||||
|
return 'b'; // Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
return $candidates[random_int(0, count($candidates) - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a vowel phoneme
|
||||||
|
*/
|
||||||
|
private function selectVowel(int $maxLength = PHP_INT_MAX): string
|
||||||
|
{
|
||||||
|
$candidates = [];
|
||||||
|
|
||||||
|
foreach ($this->vowels as $vowel) {
|
||||||
|
if (strlen($vowel) <= $maxLength) {
|
||||||
|
$candidates[] = $vowel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($candidates)) {
|
||||||
|
return 'a'; // Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
return $candidates[random_int(0, count($candidates) - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add capitalization to password (typically first letter)
|
||||||
|
*/
|
||||||
|
private function addCapitalization(string $password): string
|
||||||
|
{
|
||||||
|
if (strlen($password) === 0) {
|
||||||
|
return $password;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capitalize first letter
|
||||||
|
return ucfirst(strtolower($password));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add numbers to password if there's remaining length
|
||||||
|
*/
|
||||||
|
private function addNumbers(string $password, int $targetLength): string
|
||||||
|
{
|
||||||
|
$remaining = $targetLength - strlen($password);
|
||||||
|
|
||||||
|
if ($remaining <= 0) {
|
||||||
|
return $password;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add 1-2 numbers
|
||||||
|
$numbersToAdd = min($remaining, random_int(1, 2));
|
||||||
|
|
||||||
|
for ($i = 0; $i < $numbersToAdd; $i++) {
|
||||||
|
$password .= (string)random_int(0, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $password;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a password meets basic pronounceability rules
|
||||||
|
*/
|
||||||
|
public function isPronounceablePassword(string $password): bool
|
||||||
|
{
|
||||||
|
if (strlen($password) < 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$vowelCount = 0;
|
||||||
|
$consonantCount = 0;
|
||||||
|
|
||||||
|
// Simple heuristic: check for reasonable vowel/consonant distribution
|
||||||
|
for ($i = 0; $i < strlen($password); $i++) {
|
||||||
|
$char = strtolower($password[$i]);
|
||||||
|
if (in_array($char, ['a', 'e', 'i', 'o', 'u', 'y'])) {
|
||||||
|
$vowelCount++;
|
||||||
|
} elseif (ctype_alpha($char)) {
|
||||||
|
$consonantCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must have at least one vowel and one consonant
|
||||||
|
// And vowels shouldn't be more than 60% of letters
|
||||||
|
$totalLetters = $vowelCount + $consonantCount;
|
||||||
|
return $vowelCount >= 1 && $consonantCount >= 1 &&
|
||||||
|
($totalLetters === 0 || $vowelCount / $totalLetters <= 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available phoneme elements (for debugging/inspection)
|
||||||
|
*/
|
||||||
|
public function getPhonemeCount(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'vowels' => count(array_unique($this->vowels)),
|
||||||
|
'consonants' => count(array_unique($this->consonants)),
|
||||||
|
'total_elements' => count($this->elements)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,462 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Generators;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use App\Services\Logging\LoggingHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentic Diceware Password Generator
|
||||||
|
*
|
||||||
|
* Implements the true EFF Diceware method using cryptographically
|
||||||
|
* secure dice roll simulation. Focused on maximum security and
|
||||||
|
* authentic Diceware methodology with educational features.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class DicewareGenerator extends AbstractPasswordGenerator
|
||||||
|
{
|
||||||
|
private const GENERATOR_ID = 'diceware';
|
||||||
|
private const DICEWARE_KEYSPACE = 7776; // 6^5
|
||||||
|
|
||||||
|
private array $wordlist = [];
|
||||||
|
private array $lastDiceRolls = []; // Store for display
|
||||||
|
private int $effectiveWordlistSize = 0;
|
||||||
|
|
||||||
|
public function __construct(?LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
parent::__construct($logger);
|
||||||
|
$this->loadWordlist();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return self::GENERATOR_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVersion(): string
|
||||||
|
{
|
||||||
|
return '2.0.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return 'Diceware';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Authentic EFF Diceware method using cryptographically secure dice rolls for maximum security';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized option definitions
|
||||||
|
*/
|
||||||
|
public function getOptionDefinitions(): array
|
||||||
|
{
|
||||||
|
$options = [
|
||||||
|
'words' => [
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => 6,
|
||||||
|
'min' => 4,
|
||||||
|
'max' => 12,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'basic',
|
||||||
|
'description' => 'Number of words to include in the passphrase (4-12 recommended for security).'
|
||||||
|
],
|
||||||
|
'show_dice_rolls' => [
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => false,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'advanced',
|
||||||
|
'description' => 'Whether to display the dice rolls used to select each word (educational purpose).'
|
||||||
|
],
|
||||||
|
'uppercase_first' => [
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => false,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'advanced',
|
||||||
|
'description' => 'Capitalize the first word only (traditional Diceware style).'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generate(int $count = 1, array $options = []): array
|
||||||
|
{
|
||||||
|
$options = array_merge($this->getDefaultOptions(), $options);
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
$result = $this->generateSingle($options);
|
||||||
|
$results[] = $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateSingle(array $options): array
|
||||||
|
{
|
||||||
|
$wordCount = $options['words'];
|
||||||
|
$this->lastDiceRolls = [];
|
||||||
|
|
||||||
|
$selectedWords = [];
|
||||||
|
$diceRollsDisplay = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $wordCount; $i++) {
|
||||||
|
// True Diceware: 5 dice rolls per word
|
||||||
|
$diceRoll = $this->rollSecureDice(5);
|
||||||
|
$index = implode('', $diceRoll);
|
||||||
|
|
||||||
|
$this->lastDiceRolls[] = $diceRoll;
|
||||||
|
$diceRollsDisplay[] = $index;
|
||||||
|
|
||||||
|
if (isset($this->wordlist[$index])) {
|
||||||
|
$wordEntry = $this->wordlist[$index];
|
||||||
|
if (is_array($wordEntry)) {
|
||||||
|
$selectedWords[] = $wordEntry; // Word with explanation
|
||||||
|
} else {
|
||||||
|
$selectedWords[] = ['word' => $wordEntry, 'explanation' => null];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to random selection if index not found
|
||||||
|
$randomIndex = array_rand($this->wordlist);
|
||||||
|
$wordEntry = $this->wordlist[$randomIndex];
|
||||||
|
$selectedWords[] = is_array($wordEntry)
|
||||||
|
? $wordEntry
|
||||||
|
: ['word' => $wordEntry, 'explanation' => null];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract words for password
|
||||||
|
$words = array_map(fn($entry) => $entry['word'], $selectedWords);
|
||||||
|
|
||||||
|
// Optional: Capitalize first word only (traditional Diceware style)
|
||||||
|
if ($options['uppercase_first']) {
|
||||||
|
$words[0] = ucfirst($words[0]);
|
||||||
|
for ($i = 1; $i < count($words); $i++) {
|
||||||
|
$words[$i] = strtolower($words[$i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diceware standard: space separation (no randomization)
|
||||||
|
$password = implode(' ', $words);
|
||||||
|
|
||||||
|
$result = [
|
||||||
|
'password' => $password,
|
||||||
|
'wordEntries' => $selectedWords
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add dice rolls and security info if requested
|
||||||
|
if ($options['show_dice_rolls']) {
|
||||||
|
$result['dice_rolls'] = $diceRollsDisplay;
|
||||||
|
$result['security_info'] = $this->calculateDicewareEntropy($wordCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cryptographically secure dice rolling
|
||||||
|
* Uses true randomness for authentic Diceware methodology
|
||||||
|
*/
|
||||||
|
private function rollSecureDice(int $diceCount): array
|
||||||
|
{
|
||||||
|
$rolls = [];
|
||||||
|
for ($i = 0; $i < $diceCount; $i++) {
|
||||||
|
$rolls[] = random_int(1, 6);
|
||||||
|
}
|
||||||
|
return $rolls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate Diceware entropy and security metrics
|
||||||
|
*/
|
||||||
|
private function calculateDicewareEntropy(int $wordCount): array
|
||||||
|
{
|
||||||
|
// Use effective candidate count loaded at runtime.
|
||||||
|
$wordsInList = max(1, $this->effectiveWordlistSize);
|
||||||
|
$entropy = $wordCount * log($wordsInList, 2);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'entropy_bits' => round($entropy, 1),
|
||||||
|
'words_in_list' => $wordsInList,
|
||||||
|
'combinations' => number_format(pow($wordsInList, $wordCount)),
|
||||||
|
'security_level' => $this->getSecurityLevel($entropy),
|
||||||
|
'crack_time_estimate' => $this->getCrackTimeEstimate($entropy)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSecurityLevel(float $entropy): string
|
||||||
|
{
|
||||||
|
if ($entropy >= 80) return 'Excellent (suitable for high-value accounts)';
|
||||||
|
if ($entropy >= 65) return 'Strong (suitable for most uses)';
|
||||||
|
if ($entropy >= 50) return 'Adequate (basic security)';
|
||||||
|
return 'Weak (not recommended)';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCrackTimeEstimate(float $entropy): string
|
||||||
|
{
|
||||||
|
// Assuming 1 trillion guesses per second
|
||||||
|
$combinations = pow(2, $entropy);
|
||||||
|
$guessesPerSecond = 1e12;
|
||||||
|
$secondsToHack = $combinations / (2 * $guessesPerSecond); // Average time
|
||||||
|
|
||||||
|
if ($secondsToHack < 60) return 'Less than 1 minute';
|
||||||
|
if ($secondsToHack < 3600) return round($secondsToHack / 60) . ' minutes';
|
||||||
|
if ($secondsToHack < 86400) return round($secondsToHack / 3600) . ' hours';
|
||||||
|
if ($secondsToHack < 31536000) return round($secondsToHack / 86400) . ' days';
|
||||||
|
if ($secondsToHack < 31536000000) return round($secondsToHack / 31536000) . ' years';
|
||||||
|
return 'Millions of years';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function selectWords(int $count): array
|
||||||
|
{
|
||||||
|
// This helper remains available for the current diceware flow; generation is handled by the main path below.
|
||||||
|
if (empty($this->wordlist)) {
|
||||||
|
throw new \RuntimeException('Diceware wordlist is not loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
$wordEntries = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
$diceRoll = $this->rollSecureDice(5);
|
||||||
|
$index = $this->diceRollToIndex($diceRoll);
|
||||||
|
|
||||||
|
if (isset($this->wordlist[$index])) {
|
||||||
|
$wordEntry = $this->wordlist[$index];
|
||||||
|
$wordEntries[] = is_array($wordEntry) ? $wordEntry : ['word' => $wordEntry, 'explanation' => null];
|
||||||
|
} else {
|
||||||
|
$randomIndex = array_rand($this->wordlist);
|
||||||
|
$wordEntry = $this->wordlist[$randomIndex];
|
||||||
|
$wordEntries[] = is_array($wordEntry) ? $wordEntry : ['word' => $wordEntry, 'explanation' => null];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $wordEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function diceRollToIndex(array $rolls): string
|
||||||
|
{
|
||||||
|
return implode('', $rolls);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load EFF Diceware wordlist with explanation support
|
||||||
|
* Supports both simple words and word\texplanation format
|
||||||
|
*/
|
||||||
|
private function loadWordlist(): void
|
||||||
|
{
|
||||||
|
$candidates = [];
|
||||||
|
|
||||||
|
// Preferred native Diceware-format file (11111 word).
|
||||||
|
$primaryPath = __DIR__ . '/../../data/wordlists/eff_large_wordlist.txt';
|
||||||
|
$this->collectWordsFromFile($primaryPath, $candidates);
|
||||||
|
|
||||||
|
// Augment with repository wordlists to ensure robust candidate space.
|
||||||
|
$augmentPaths = [
|
||||||
|
__DIR__ . '/../../data/wordlists/eff_large_wordlist_en.txt',
|
||||||
|
__DIR__ . '/../../data/wordlists/german_common_words_de.txt',
|
||||||
|
__DIR__ . '/../../data/wordlists/french_common_words_fr.txt',
|
||||||
|
__DIR__ . '/../../data/wordlists/italian_common_words_it.txt',
|
||||||
|
__DIR__ . '/../../data/wordlists/spanish_common_words_es.txt',
|
||||||
|
__DIR__ . '/../../data/wordlists/swiss_german_slaengikon.txt',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($augmentPaths as $path) {
|
||||||
|
$this->collectWordsFromFile($path, $candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($candidates)) {
|
||||||
|
$this->loadFallbackWordlist();
|
||||||
|
$this->effectiveWordlistSize = count($this->wordlist);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidateList = array_values($candidates);
|
||||||
|
$candidateCount = count($candidateList);
|
||||||
|
$keys = $this->generateDicewareKeys();
|
||||||
|
|
||||||
|
$this->wordlist = [];
|
||||||
|
foreach ($keys as $index => $diceKey) {
|
||||||
|
$this->wordlist[$diceKey] = $candidateList[$index % $candidateCount];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->effectiveWordlistSize = min($candidateCount, self::DICEWARE_KEYSPACE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate accurate entropy for Diceware generation
|
||||||
|
*/
|
||||||
|
public function calculateEntropyForOptions(array $options): float
|
||||||
|
{
|
||||||
|
$breakdown = $this->getEntropyBreakdownForOptions($options);
|
||||||
|
return (float) ($breakdown['entropy_bits'] ?? 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detailed entropy contributors for API transparency.
|
||||||
|
*/
|
||||||
|
public function getEntropyBreakdownForOptions(array $options): array
|
||||||
|
{
|
||||||
|
$validated = $this->validateOptions($options);
|
||||||
|
$words = max(1, (int) ($validated['words'] ?? 6));
|
||||||
|
|
||||||
|
// Candidate space reflects the loaded wordlist quality.
|
||||||
|
$wordlistSize = max(1, $this->effectiveWordlistSize);
|
||||||
|
$wordEntropyBits = $words * log((float) $wordlistSize, 2);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'entropy_model' => 'diceware-combinatorial-v1',
|
||||||
|
'model_confidence' => $wordlistSize >= self::DICEWARE_KEYSPACE ? 'high' : 'medium',
|
||||||
|
'requested_words' => $words,
|
||||||
|
'wordlist_size' => $wordlistSize,
|
||||||
|
'word_entropy_bits' => round($wordEntropyBits, 1),
|
||||||
|
'separator_entropy_bits' => 0.0,
|
||||||
|
'number_entropy_bits' => 0.0,
|
||||||
|
'entropy_bits' => round($wordEntropyBits, 1),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load fallback wordlist with explanations
|
||||||
|
*/
|
||||||
|
private function loadFallbackWordlist(): void
|
||||||
|
{
|
||||||
|
// Fallback wordlist with explanations for demonstration
|
||||||
|
$fallbackWords = [
|
||||||
|
'Aardvark' => 'nocturnal mammal that feeds on ants',
|
||||||
|
'absurd' => 'wildly unreasonable or illogical',
|
||||||
|
'accrue' => 'accumulate or receive over time',
|
||||||
|
'Acme' => 'the highest point of achievement',
|
||||||
|
'adrift' => 'floating without being steered',
|
||||||
|
'Adult' => 'fully grown person',
|
||||||
|
'afflict' => 'cause pain or suffering to',
|
||||||
|
'ahead' => 'in front or in advance',
|
||||||
|
'aimless' => 'without purpose or direction',
|
||||||
|
'Algae' => 'simple plants found in water',
|
||||||
|
'allow' => 'permit or give permission',
|
||||||
|
'alone' => 'on one\'s own without others',
|
||||||
|
'amuse' => 'provide entertainment or cause laughter',
|
||||||
|
'Angel' => 'spiritual being or very kind person',
|
||||||
|
'Ankle' => 'joint connecting foot and leg',
|
||||||
|
'Apathy' => 'lack of interest or enthusiasm',
|
||||||
|
'aqua' => 'light blue-green color like water',
|
||||||
|
'Arcade' => 'covered passage with shops',
|
||||||
|
'ardent' => 'passionate or enthusiastic',
|
||||||
|
'Area' => 'region or extent of space',
|
||||||
|
'argue' => 'give reasons for or against something',
|
||||||
|
'Ark' => 'large boat or chest',
|
||||||
|
'armed' => 'equipped with weapons',
|
||||||
|
'Armor' => 'protective covering or clothing',
|
||||||
|
'Army' => 'organized military force',
|
||||||
|
'around' => 'on every side or in the vicinity',
|
||||||
|
'Arrow' => 'projectile shot from a bow',
|
||||||
|
'Arson' => 'crime of deliberately setting fire',
|
||||||
|
'Art' => 'creative expression or skill',
|
||||||
|
'ascend' => 'go up or climb higher',
|
||||||
|
'ask' => 'request information or something',
|
||||||
|
'Aspect' => 'particular part or feature',
|
||||||
|
'Atom' => 'smallest unit of matter',
|
||||||
|
'Attic' => 'space under the roof',
|
||||||
|
'Avatar' => 'representation of a person online',
|
||||||
|
'avoid' => 'keep away from or prevent',
|
||||||
|
'awake' => 'not sleeping or alert',
|
||||||
|
'Award' => 'prize given for achievement',
|
||||||
|
'aware' => 'conscious or informed',
|
||||||
|
'away' => 'at a distance from',
|
||||||
|
'Axis' => 'imaginary line through center',
|
||||||
|
'Backup' => 'copy kept for security',
|
||||||
|
'Badge' => 'symbol of authority or achievement',
|
||||||
|
'Bagel' => 'ring-shaped bread roll',
|
||||||
|
'Balance' => 'even distribution of weight',
|
||||||
|
'Ball' => 'round object used in games',
|
||||||
|
'Bamboo' => 'fast-growing woody grass',
|
||||||
|
'Banana' => 'yellow curved tropical fruit',
|
||||||
|
'Band' => 'group or strip of material',
|
||||||
|
'Banner' => 'flag or sign for display',
|
||||||
|
'bring' => 'take or carry to a place'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Convert to Diceware index format for consistency
|
||||||
|
$index = 11111;
|
||||||
|
foreach ($fallbackWords as $word => $explanation) {
|
||||||
|
$this->wordlist[(string)$index] = [
|
||||||
|
'word' => $word,
|
||||||
|
'explanation' => $explanation
|
||||||
|
];
|
||||||
|
$index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->effectiveWordlistSize = count($this->wordlist);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect word entries from a wordlist file and deduplicate by lowercase word.
|
||||||
|
*/
|
||||||
|
private function collectWordsFromFile(string $path, array &$target): void
|
||||||
|
{
|
||||||
|
if (!file_exists($path) || !is_readable($path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($path);
|
||||||
|
if ($content === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = explode("\n", $content);
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if ($line === '' || str_starts_with($line, '#')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supports classic Diceware lines: 12345 word[\texplanation]
|
||||||
|
if (preg_match('/^(\d{5})\s+(.+)$/', $line, $matches)) {
|
||||||
|
$entry = $this->parseWordlistEntry(trim($matches[2]));
|
||||||
|
} else {
|
||||||
|
$entry = $this->parseWordlistEntry($line);
|
||||||
|
}
|
||||||
|
|
||||||
|
$word = trim((string) ($entry['word'] ?? ''));
|
||||||
|
if ($word === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = strtolower($word);
|
||||||
|
if (!isset($target[$key])) {
|
||||||
|
$target[$key] = [
|
||||||
|
'word' => $word,
|
||||||
|
'explanation' => $entry['explanation'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate all valid 5-dice key strings from 11111 to 66666.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function generateDicewareKeys(): array
|
||||||
|
{
|
||||||
|
$keys = [];
|
||||||
|
|
||||||
|
for ($a = 1; $a <= 6; $a++) {
|
||||||
|
for ($b = 1; $b <= 6; $b++) {
|
||||||
|
for ($c = 1; $c <= 6; $c++) {
|
||||||
|
for ($d = 1; $d <= 6; $d++) {
|
||||||
|
for ($e = 1; $e <= 6; $e++) {
|
||||||
|
$keys[] = (string) ($a . $b . $c . $d . $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $keys;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Generators;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* High Entropy Password Generator
|
||||||
|
*
|
||||||
|
* Generates cryptographically secure random passwords with customizable
|
||||||
|
* character sets based on user preferences.
|
||||||
|
*/
|
||||||
|
class HighEntropyGenerator extends AbstractPasswordGenerator
|
||||||
|
{
|
||||||
|
private const GENERATOR_ID = 'high_entropy';
|
||||||
|
|
||||||
|
private const LOWERCASE = 'abcdefghijklmnopqrstuvwxyz';
|
||||||
|
private const UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
private const NUMBERS = '0123456789';
|
||||||
|
private const SYMBOLS = '!@#$%^&*()_+-=[]{}|;:,.<>?';
|
||||||
|
|
||||||
|
public function __construct(?LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
parent::__construct($logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return self::GENERATOR_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVersion(): string
|
||||||
|
{
|
||||||
|
return '1.1.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return 'High Entropy';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Cryptographically secure random passwords with customizable character sets';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized option definitions
|
||||||
|
*/
|
||||||
|
public function getOptionDefinitions(): array
|
||||||
|
{
|
||||||
|
$options = [
|
||||||
|
'length' => [
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => 16,
|
||||||
|
'min' => 8,
|
||||||
|
'max' => 128,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'basic',
|
||||||
|
'description' => 'Total length of the generated password'
|
||||||
|
],
|
||||||
|
'include_uppercase' => [
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'character_set',
|
||||||
|
'description' => 'Include uppercase letters (A-Z)'
|
||||||
|
],
|
||||||
|
'include_lowercase' => [
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'character_set',
|
||||||
|
'description' => 'Include lowercase letters (a-z)'
|
||||||
|
],
|
||||||
|
'include_numbers' => [
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'character_set',
|
||||||
|
'description' => 'Include numbers (0-9)'
|
||||||
|
],
|
||||||
|
'include_symbols' => [
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'character_set',
|
||||||
|
'description' => 'Include symbols (e.g., !@#$%)'
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generate(int $count = 1, array $options = []): array
|
||||||
|
{
|
||||||
|
$options = array_merge($this->getDefaultOptions(), $options);
|
||||||
|
$passwords = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
$passwords[] = $this->generateSingle($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $passwords;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateSingle(array $options): string
|
||||||
|
{
|
||||||
|
$characterSet = $this->buildCharacterSet($options);
|
||||||
|
$length = $options['length'];
|
||||||
|
|
||||||
|
if (empty($characterSet)) {
|
||||||
|
throw new \RuntimeException('No character types selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate password ensuring at least one character from each selected type
|
||||||
|
$password = $this->generateWithGuaranteedTypes($characterSet, $length, $options);
|
||||||
|
|
||||||
|
// Shuffle the password to randomize position of guaranteed characters
|
||||||
|
return $this->shuffleString($password);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildCharacterSet(array $options): string
|
||||||
|
{
|
||||||
|
$characterSet = '';
|
||||||
|
|
||||||
|
if ($options['include_lowercase'] ?? false) {
|
||||||
|
$characterSet .= self::LOWERCASE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($options['include_uppercase'] ?? false) {
|
||||||
|
$characterSet .= self::UPPERCASE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($options['include_numbers'] ?? false) {
|
||||||
|
$characterSet .= self::NUMBERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($options['include_symbols'] ?? false) {
|
||||||
|
$characterSet .= self::SYMBOLS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $characterSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateWithGuaranteedTypes(string $characterSet, int $length, array $options): string
|
||||||
|
{
|
||||||
|
$password = '';
|
||||||
|
$guaranteedChars = [];
|
||||||
|
|
||||||
|
// Add at least one character from each selected type
|
||||||
|
if ($options['include_lowercase'] ?? false) {
|
||||||
|
$guaranteedChars[] = self::LOWERCASE[random_int(0, strlen(self::LOWERCASE) - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($options['include_uppercase'] ?? false) {
|
||||||
|
$guaranteedChars[] = self::UPPERCASE[random_int(0, strlen(self::UPPERCASE) - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($options['include_numbers'] ?? false) {
|
||||||
|
$guaranteedChars[] = self::NUMBERS[random_int(0, strlen(self::NUMBERS) - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($options['include_symbols'] ?? false) {
|
||||||
|
$guaranteedChars[] = self::SYMBOLS[random_int(0, strlen(self::SYMBOLS) - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add guaranteed characters
|
||||||
|
$password = implode('', $guaranteedChars);
|
||||||
|
|
||||||
|
// Fill remaining length with random characters from full set
|
||||||
|
$remainingLength = $length - count($guaranteedChars);
|
||||||
|
for ($i = 0; $i < $remainingLength; $i++) {
|
||||||
|
$password .= $characterSet[random_int(0, strlen($characterSet) - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $password;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shuffleString(string $string): string
|
||||||
|
{
|
||||||
|
$characters = str_split($string);
|
||||||
|
|
||||||
|
// Fisher-Yates shuffle using cryptographically secure random
|
||||||
|
for ($i = count($characters) - 1; $i > 0; $i--) {
|
||||||
|
$j = random_int(0, $i);
|
||||||
|
[$characters[$i], $characters[$j]] = [$characters[$j], $characters[$i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode('', $characters);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Generators\Interfaces;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for generator option schema definitions
|
||||||
|
*
|
||||||
|
* All option types must implement this interface to ensure
|
||||||
|
* consistent metadata and i18n support across generators.
|
||||||
|
*
|
||||||
|
* @package App\Generators\Interfaces
|
||||||
|
*/
|
||||||
|
interface GeneratorOptionInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the option name (internal identifier)
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getName(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the option type (integer, boolean, string, select)
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getType(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get i18n key for the option label
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getLabelKey(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get i18n key for the option description/help text
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getDescriptionKey(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default value for this option
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getDefault(): mixed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this option is required
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isRequired(): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the option group (basic, advanced, character_set, etc.)
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getGroup(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get i18n key for the group title
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getGroupTitleKey(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert option to array format for API responses
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function toArray(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a value against this option's constraints
|
||||||
|
* @param mixed $value
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function validate(mixed $value): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get validation error message i18n key
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getValidationErrorKey(): string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Generators;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apple-Style Passkey Generator
|
||||||
|
*
|
||||||
|
* Generates pronounceable passwords using syllable patterns with
|
||||||
|
* integrated character substitutions, similar to Apple Keychain suggestions.
|
||||||
|
*
|
||||||
|
* REFACTORING CHANGES APPLIED:
|
||||||
|
* - Step 5: Removed non-functional add_numbers and add_symbols options
|
||||||
|
* - Focus on syllable-based generation with integrated substitutions
|
||||||
|
* - Apple-style formatting with character replacements (o→0, i→1, etc.)
|
||||||
|
*
|
||||||
|
* FIXES APPLIED:
|
||||||
|
* - Removed calculateEntropy method that conflicted with parent signature
|
||||||
|
* - Fixed generate() method to return simple string array (not objects)
|
||||||
|
*/
|
||||||
|
class PasskeyGenerator extends AbstractPasswordGenerator
|
||||||
|
{
|
||||||
|
private const GENERATOR_ID = 'passkey';
|
||||||
|
|
||||||
|
private const CONSONANTS = 'bcdfghjklmnpqrstvwxyz';
|
||||||
|
private const VOWELS = 'aeiou';
|
||||||
|
|
||||||
|
// Common consonant pairs that sound natural together
|
||||||
|
private const CONSONANT_PAIRS = [
|
||||||
|
'bl',
|
||||||
|
'br',
|
||||||
|
'ch',
|
||||||
|
'cl',
|
||||||
|
'cr',
|
||||||
|
'dr',
|
||||||
|
'fl',
|
||||||
|
'fr',
|
||||||
|
'gl',
|
||||||
|
'gr',
|
||||||
|
'pl',
|
||||||
|
'pr',
|
||||||
|
'sc',
|
||||||
|
'sh',
|
||||||
|
'sk',
|
||||||
|
'sl',
|
||||||
|
'sm',
|
||||||
|
'sn',
|
||||||
|
'sp',
|
||||||
|
'st',
|
||||||
|
'sw',
|
||||||
|
'th',
|
||||||
|
'tr',
|
||||||
|
'tw',
|
||||||
|
'wh',
|
||||||
|
'wr'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Syllable patterns for natural pronunciation
|
||||||
|
private const PATTERNS = [
|
||||||
|
'cv', // consonant-vowel (most common)
|
||||||
|
'cvc', // consonant-vowel-consonant
|
||||||
|
'ccv', // consonant-consonant-vowel
|
||||||
|
'cvcv', // consonant-vowel-consonant-vowel (longer syllables)
|
||||||
|
'vc', // vowel-consonant (occasional)
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(?LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
parent::__construct($logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return self::GENERATOR_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVersion(): string
|
||||||
|
{
|
||||||
|
return '1.2.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return 'Passkey';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Apple-style pronounceable passwords with integrated character substitutions';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized option definitions
|
||||||
|
*/
|
||||||
|
public function getOptionDefinitions(): array
|
||||||
|
{
|
||||||
|
$options = [
|
||||||
|
'syllables' => [
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => 6,
|
||||||
|
'min' => 4,
|
||||||
|
'max' => 12,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'basic',
|
||||||
|
'description' => 'Number of pronounceable syllables in the passkey'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXED: Return simple array of password strings (not objects)
|
||||||
|
* This matches the expected format for both API and frontend
|
||||||
|
*/
|
||||||
|
public function generate(int $count = 1, array $options = []): array
|
||||||
|
{
|
||||||
|
$options = array_merge($this->getDefaultOptions(), $options);
|
||||||
|
$passwords = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
$passwords[] = $this->generateSingle($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $passwords;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateSingle(array $options): string
|
||||||
|
{
|
||||||
|
$syllableCount = $options['syllables'];
|
||||||
|
|
||||||
|
// Generate pronounceable syllables
|
||||||
|
$syllables = $this->generateSyllables($syllableCount);
|
||||||
|
|
||||||
|
// Apply Apple-style formatting with integrated character substitutions
|
||||||
|
$password = $this->formatAppleStyle($syllables);
|
||||||
|
|
||||||
|
return $password;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate pronounceable syllables using various patterns
|
||||||
|
*/
|
||||||
|
private function generateSyllables(int $count): array
|
||||||
|
{
|
||||||
|
$syllables = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
// Select a random syllable pattern
|
||||||
|
$pattern = self::PATTERNS[random_int(0, count(self::PATTERNS) - 1)];
|
||||||
|
$syllable = $this->generateSyllableByPattern($pattern);
|
||||||
|
$syllables[] = $syllable;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $syllables;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a syllable following a specific pattern
|
||||||
|
*/
|
||||||
|
private function generateSyllableByPattern(string $pattern): string
|
||||||
|
{
|
||||||
|
$syllable = '';
|
||||||
|
$i = 0;
|
||||||
|
|
||||||
|
while ($i < strlen($pattern)) {
|
||||||
|
$char = $pattern[$i];
|
||||||
|
|
||||||
|
if ($char === 'c') {
|
||||||
|
// Check if we can use a consonant pair
|
||||||
|
if ($i === 0 && random_int(1, 4) === 1) { // 25% chance
|
||||||
|
$pair = self::CONSONANT_PAIRS[random_int(0, count(self::CONSONANT_PAIRS) - 1)];
|
||||||
|
$syllable .= $pair;
|
||||||
|
// Skip next consonant if it exists in pattern
|
||||||
|
if ($i + 1 < strlen($pattern) && $pattern[$i + 1] === 'c') {
|
||||||
|
$i++; // Skip the next 'c' since we used a pair
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$consonant = self::CONSONANTS[random_int(0, strlen(self::CONSONANTS) - 1)];
|
||||||
|
$syllable .= $consonant;
|
||||||
|
}
|
||||||
|
} else if ($char === 'v') {
|
||||||
|
$vowel = self::VOWELS[random_int(0, strlen(self::VOWELS) - 1)];
|
||||||
|
$syllable .= $vowel;
|
||||||
|
}
|
||||||
|
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $syllable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply Apple-style formatting with integrated character substitutions
|
||||||
|
*/
|
||||||
|
private function formatAppleStyle(array $syllables): string
|
||||||
|
{
|
||||||
|
$password = implode('', $syllables);
|
||||||
|
|
||||||
|
// Apple-style character substitutions (looks similar, maintains pronounceability)
|
||||||
|
$substitutions = [
|
||||||
|
'o' => '0', // o looks like 0
|
||||||
|
'i' => '1', // i looks like 1
|
||||||
|
'a' => '@', // a looks like @
|
||||||
|
's' => '$', // s looks like $
|
||||||
|
'e' => '3', // e looks like 3
|
||||||
|
];
|
||||||
|
|
||||||
|
// Apply 1-2 substitutions randomly for Apple-like feel (reduced from 1-3)
|
||||||
|
$maxSubstitutions = min(2, max(1, strlen($password) / 4)); // More conservative
|
||||||
|
$substitutionCount = random_int(1, $maxSubstitutions);
|
||||||
|
$applied = 0;
|
||||||
|
|
||||||
|
// Shuffle substitutions for randomness
|
||||||
|
$availableSubstitutions = $substitutions;
|
||||||
|
$keys = array_keys($availableSubstitutions);
|
||||||
|
shuffle($keys);
|
||||||
|
|
||||||
|
foreach ($keys as $letter) {
|
||||||
|
if ($applied >= $substitutionCount) break;
|
||||||
|
|
||||||
|
$replacement = $substitutions[$letter];
|
||||||
|
|
||||||
|
// Find all positions of this letter
|
||||||
|
$positions = [];
|
||||||
|
for ($i = 0; $i < strlen($password); $i++) {
|
||||||
|
if (strtolower($password[$i]) === $letter) { // Case insensitive search
|
||||||
|
$positions[] = $i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($positions)) {
|
||||||
|
// Replace one random occurrence of this letter
|
||||||
|
$randomPosition = $positions[random_int(0, count($positions) - 1)];
|
||||||
|
$password[$randomPosition] = $replacement;
|
||||||
|
$applied++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apple-style: Capitalize first letter
|
||||||
|
$password = ucfirst($password);
|
||||||
|
|
||||||
|
return $password;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate entropy for syllable-based generation
|
||||||
|
*/
|
||||||
|
public function calculateEntropyForOptions(array $options): float
|
||||||
|
{
|
||||||
|
$breakdown = $this->getEntropyBreakdownForOptions($options);
|
||||||
|
return (float) ($breakdown['entropy_bits'] ?? 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detailed entropy contributors for API transparency.
|
||||||
|
*/
|
||||||
|
public function getEntropyBreakdownForOptions(array $options): array
|
||||||
|
{
|
||||||
|
$validated = $this->validateOptions($options);
|
||||||
|
$syllables = max(1, (int) ($validated['syllables'] ?? 6));
|
||||||
|
|
||||||
|
// Heuristic estimate based on average pronounceable-syllable branching.
|
||||||
|
$combinationsPerSyllable = 150.0;
|
||||||
|
$baseEntropyBits = $syllables * log($combinationsPerSyllable, 2);
|
||||||
|
|
||||||
|
// Character substitutions add a small, heuristic bonus.
|
||||||
|
$substitutionMultiplier = 1.2;
|
||||||
|
$substitutionEntropyBits = log($substitutionMultiplier, 2);
|
||||||
|
|
||||||
|
$totalEntropyBits = $baseEntropyBits + $substitutionEntropyBits;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'entropy_model' => 'passkey-syllable-heuristic-v1',
|
||||||
|
'model_confidence' => 'medium',
|
||||||
|
'requested_syllables' => $syllables,
|
||||||
|
'estimated_combinations_per_syllable' => $combinationsPerSyllable,
|
||||||
|
'substitution_multiplier' => $substitutionMultiplier,
|
||||||
|
'syllable_entropy_bits' => round($baseEntropyBits, 1),
|
||||||
|
'substitution_entropy_bits' => round($substitutionEntropyBits, 1),
|
||||||
|
'entropy_bits' => round($totalEntropyBits, 1),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get example passkeys for documentation
|
||||||
|
*/
|
||||||
|
public function getExamples(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Short (4 syllables)' => $this->generate(3, ['syllables' => 4]),
|
||||||
|
'Medium (6 syllables)' => $this->generate(3, ['syllables' => 6]),
|
||||||
|
'Long (8 syllables)' => $this->generate(3, ['syllables' => 8]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pronunciation guide for a generated password
|
||||||
|
*/
|
||||||
|
public function getPronunciationGuide(string $password): string
|
||||||
|
{
|
||||||
|
// Remove numbers and symbols for pronunciation
|
||||||
|
$pronounceable = preg_replace('/[0-9@$3]/', '', $password);
|
||||||
|
|
||||||
|
// Add hyphens between likely syllable breaks
|
||||||
|
$guide = '';
|
||||||
|
$vowelPattern = '/[aeiou]/i';
|
||||||
|
$lastWasVowel = false;
|
||||||
|
|
||||||
|
for ($i = 0; $i < strlen($pronounceable); $i++) {
|
||||||
|
$char = $pronounceable[$i];
|
||||||
|
$isVowel = preg_match($vowelPattern, $char);
|
||||||
|
|
||||||
|
// Add hyphen before consonants that follow vowels (syllable breaks)
|
||||||
|
if (!$isVowel && $lastWasVowel && $i > 0) {
|
||||||
|
$guide .= '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
$guide .= strtolower($char);
|
||||||
|
$lastWasVowel = $isVowel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $guide;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Generators;
|
||||||
|
|
||||||
|
use App\Generators\Interfaces\GeneratorOptionInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for all password generators with centralized option definitions
|
||||||
|
* and programmatic i18n key derivation
|
||||||
|
*
|
||||||
|
* All generators must define their options in getOptionDefinitions() and
|
||||||
|
* i18n keys will be automatically derived from the generator ID and option names.
|
||||||
|
*
|
||||||
|
* @package App\Generators
|
||||||
|
*/
|
||||||
|
interface PasswordGeneratorInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Generate passwords
|
||||||
|
*
|
||||||
|
* @param int $count Number of passwords to generate
|
||||||
|
* @param array $options Generation options
|
||||||
|
* @return array Array of password data structures
|
||||||
|
*/
|
||||||
|
public function generate(int $count = 1, array $options = []): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get generator ID (internal identifier)
|
||||||
|
* Used as base for all i18n keys
|
||||||
|
*
|
||||||
|
* @return string Generator ID (e.g., 'pwgen', 'xkcdpass')
|
||||||
|
*/
|
||||||
|
public function getId(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get generator version
|
||||||
|
*
|
||||||
|
* @return string Version string (e.g., '3.1.0')
|
||||||
|
*/
|
||||||
|
public function getVersion(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get option definitions (centralized configuration)
|
||||||
|
*
|
||||||
|
* All option metadata, constraints, and defaults in one place.
|
||||||
|
* I18n keys will be automatically derived from generator ID and option names.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* [
|
||||||
|
* 'length' => [
|
||||||
|
* 'type' => 'integer',
|
||||||
|
* 'default' => 8,
|
||||||
|
* 'min' => 4,
|
||||||
|
* 'max' => 32,
|
||||||
|
* 'required' => false,
|
||||||
|
* 'group' => 'basic'
|
||||||
|
* ],
|
||||||
|
* 'separator' => [
|
||||||
|
* 'type' => 'string',
|
||||||
|
* 'default' => '',
|
||||||
|
* 'maxLength' => 10,
|
||||||
|
* 'group' => 'advanced'
|
||||||
|
* ],
|
||||||
|
* 'wordlist' => [
|
||||||
|
* 'type' => 'select',
|
||||||
|
* 'default' => 'en',
|
||||||
|
* 'choices' => $this->getAvailableWordlists(),
|
||||||
|
* 'translateChoices' => false, // Don't translate wordlist names
|
||||||
|
* 'group' => 'basic'
|
||||||
|
* ]
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* @return array Associative array of option definitions
|
||||||
|
*/
|
||||||
|
public function getOptionDefinitions(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get generator name i18n key (automatically derived)
|
||||||
|
*
|
||||||
|
* Pattern: {generator_id}_name
|
||||||
|
*
|
||||||
|
* @return string i18n key
|
||||||
|
*/
|
||||||
|
public function getNameKey(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get generator description i18n key (automatically derived)
|
||||||
|
*
|
||||||
|
* Pattern: {generator_id}_description
|
||||||
|
*
|
||||||
|
* @return string i18n key
|
||||||
|
*/
|
||||||
|
public function getDescriptionKey(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get i18n key for an option label (automatically derived)
|
||||||
|
*
|
||||||
|
* Pattern: {generator_id}_option_{option_name}_label
|
||||||
|
*
|
||||||
|
* @param string $optionName Option name
|
||||||
|
* @return string i18n key
|
||||||
|
*/
|
||||||
|
public function getOptionLabelKey(string $optionName): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get i18n key for an option description (automatically derived)
|
||||||
|
*
|
||||||
|
* Pattern: {generator_id}_option_{option_name}_description
|
||||||
|
*
|
||||||
|
* @param string $optionName Option name
|
||||||
|
* @return string i18n key
|
||||||
|
*/
|
||||||
|
public function getOptionDescriptionKey(string $optionName): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get i18n key for an option group title (automatically derived)
|
||||||
|
*
|
||||||
|
* Pattern: {generator_id}_options_group_{group_name}_title
|
||||||
|
*
|
||||||
|
* @param string $groupName Group name
|
||||||
|
* @return string i18n key
|
||||||
|
*/
|
||||||
|
public function getOptionGroupTitleKey(string $groupName): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get i18n key for a select option choice (automatically derived)
|
||||||
|
*
|
||||||
|
* Pattern: {generator_id}_option_{option_name}_choice_{choice_value}
|
||||||
|
*
|
||||||
|
* @param string $optionName Option name
|
||||||
|
* @param string $choiceValue Choice value
|
||||||
|
* @return string i18n key
|
||||||
|
*/
|
||||||
|
public function getOptionChoiceLabelKey(string $optionName, string $choiceValue): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get i18n key for validation error (automatically derived)
|
||||||
|
*
|
||||||
|
* Pattern: {generator_id}_option_{option_name}_validation_error
|
||||||
|
*
|
||||||
|
* @param string $optionName Option name
|
||||||
|
* @return string i18n key
|
||||||
|
*/
|
||||||
|
public function getValidationErrorKey(string $optionName): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all i18n keys used by this generator (for translation extraction)
|
||||||
|
*
|
||||||
|
* Returns array of all keys that need translations
|
||||||
|
*
|
||||||
|
* @return array Array of i18n keys
|
||||||
|
*/
|
||||||
|
public function getAllI18nKeys(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default options (derived from option definitions)
|
||||||
|
*
|
||||||
|
* @return array Associative array of option_name => default_value
|
||||||
|
*/
|
||||||
|
public function getDefaultOptions(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate options against definitions
|
||||||
|
*
|
||||||
|
* @param array $options Options to validate
|
||||||
|
* @return array Validated and sanitized options
|
||||||
|
* @throws \InvalidArgumentException If validation fails
|
||||||
|
*/
|
||||||
|
public function validateOptions(array $options): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get complete option schema for API (with derived i18n keys)
|
||||||
|
*
|
||||||
|
* @return array Array of option metadata with all i18n keys
|
||||||
|
*/
|
||||||
|
public function getOptionSchema(): array;
|
||||||
|
}
|
||||||
@@ -0,0 +1,666 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Generators;
|
||||||
|
|
||||||
|
use App\Services\LLM\LLMManager;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phrase Abbreviator Generator v1.3
|
||||||
|
*
|
||||||
|
* Generates passwords from sentences by extracting and combining first letters
|
||||||
|
* and other word characters based on customizable rules.
|
||||||
|
*
|
||||||
|
* Supports:
|
||||||
|
* - Single or dual character extraction per word
|
||||||
|
* - Optional random number addition (1-4 digits)
|
||||||
|
* - AI-generated sentences via LLM integration
|
||||||
|
* - Localized number word conversion from translation files
|
||||||
|
* - Multiple language support for number words
|
||||||
|
*
|
||||||
|
* @package App\Generators
|
||||||
|
*/
|
||||||
|
class PhraseAbbreviatorGenerator extends AbstractPasswordGenerator
|
||||||
|
{
|
||||||
|
private const GENERATOR_ID = 'phraseabbreviator';
|
||||||
|
private const DEFAULT_SENTENCE = 'The quick brown fox jumps over the lazy dog.';
|
||||||
|
private const VOWELS = 'aeiouyAEIOUY';
|
||||||
|
private const CONSONANTS = 'bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ';
|
||||||
|
private ?LLMManager $llmManager = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor with optional LLM Manager and logger
|
||||||
|
*/
|
||||||
|
public function __construct(?LLMManager $llmManager = null, ?LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
parent::__construct($logger); // ✅ Calls parent::__construct()
|
||||||
|
$this->llmManager = $llmManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return self::GENERATOR_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVersion(): string
|
||||||
|
{
|
||||||
|
return '1.3.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return 'Phrase Abbreviator';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Generates memorable passwords from sentence fragments and word abbreviations';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get option definitions
|
||||||
|
*/
|
||||||
|
public function getOptionDefinitions(): array
|
||||||
|
{
|
||||||
|
$options = [
|
||||||
|
'sentence' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => self::DEFAULT_SENTENCE,
|
||||||
|
'minLength' => 1,
|
||||||
|
'maxLength' => 500,
|
||||||
|
'required' => false, // Not required if using AI
|
||||||
|
'group' => 'basic',
|
||||||
|
'description' => 'User-supplied sentence (minimum 8 words, must start with capital letter)'
|
||||||
|
],
|
||||||
|
'character_extraction' => [
|
||||||
|
'type' => 'select',
|
||||||
|
'default' => 'single',
|
||||||
|
'choices' => [
|
||||||
|
['value' => 'single', 'label' => 'First letter only'],
|
||||||
|
['value' => 'dual', 'label' => 'First letter + next vowel/consonant']
|
||||||
|
],
|
||||||
|
'translateChoices' => true,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'basic',
|
||||||
|
'description' => 'Method for extracting characters from each word'
|
||||||
|
],
|
||||||
|
'language' => [
|
||||||
|
'type' => 'select',
|
||||||
|
'default' => 'en',
|
||||||
|
'choices' => $this->getLanguageChoices(),
|
||||||
|
'translateChoices' => false,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'basic',
|
||||||
|
'description' => 'Language for number word conversion'
|
||||||
|
],
|
||||||
|
'add_number' => [
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => false,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'advanced',
|
||||||
|
'description' => 'Add random digits to the password'
|
||||||
|
],
|
||||||
|
'number_digits' => [
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => 2,
|
||||||
|
'min' => 1,
|
||||||
|
'max' => 4,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'advanced',
|
||||||
|
'description' => 'How many random digits to add (1-4)'
|
||||||
|
],
|
||||||
|
'number_position' => [
|
||||||
|
'type' => 'select',
|
||||||
|
'default' => 'append',
|
||||||
|
'choices' => [
|
||||||
|
['value' => 'prepend', 'label' => 'Before'],
|
||||||
|
['value' => 'append', 'label' => 'After']
|
||||||
|
],
|
||||||
|
'translateChoices' => true,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'advanced',
|
||||||
|
'description' => 'Place number before or after the password'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add AI options if LLM manager is available
|
||||||
|
if ($this->llmManager && $this->llmManager->hasAvailableProvider()) {
|
||||||
|
$options['use_ai_sentence'] = [
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => false,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'ai',
|
||||||
|
'description' => 'Let AI generate a sentence from your prompt'
|
||||||
|
];
|
||||||
|
|
||||||
|
$options['ai_prompt'] = [
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => 'Generate a creative, memorable sentence about technology.',
|
||||||
|
'maxLength' => 500,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'ai',
|
||||||
|
'description' => 'Prompt for AI to generate a memorable sentence (minimum 12 words)'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Get available LLM providers
|
||||||
|
$providers = $this->llmManager->getAvailableProviders();
|
||||||
|
if (!empty($providers)) {
|
||||||
|
$providerChoices = [];
|
||||||
|
foreach ($providers as $name => $info) {
|
||||||
|
$providerChoices[] = [
|
||||||
|
'value' => $name,
|
||||||
|
'label' => $info['display_name']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$options['ai_provider'] = [
|
||||||
|
'type' => 'select',
|
||||||
|
'default' => $this->llmManager->getDefaultProviderName(),
|
||||||
|
'choices' => $providerChoices,
|
||||||
|
'translateChoices' => false,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'ai',
|
||||||
|
'description' => 'Which LLM provider to use for sentence generation'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Get models for default provider
|
||||||
|
$defaultProvider = $this->llmManager->getDefaultProvider();
|
||||||
|
$modelChoices = [];
|
||||||
|
foreach ($defaultProvider->getAvailableModels() as $value => $label) {
|
||||||
|
$modelChoices[] = ['value' => $value, 'label' => $label];
|
||||||
|
}
|
||||||
|
|
||||||
|
$options['ai_model'] = [
|
||||||
|
'type' => 'select',
|
||||||
|
'default' => $defaultProvider->getDefaultModel(),
|
||||||
|
'choices' => $modelChoices,
|
||||||
|
'translateChoices' => false,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'ai',
|
||||||
|
'description' => 'Which AI model to use'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get language choices from configuration
|
||||||
|
*/
|
||||||
|
private function getLanguageChoices(): array
|
||||||
|
{
|
||||||
|
$languages = $this->config['locale']['languages'] ?? [
|
||||||
|
'en' => 'English',
|
||||||
|
'de' => 'Deutsch',
|
||||||
|
'fr' => 'Français',
|
||||||
|
'es' => 'Español',
|
||||||
|
'it' => 'Italiano'
|
||||||
|
];
|
||||||
|
|
||||||
|
$choices = [];
|
||||||
|
foreach ($languages as $code => $name) {
|
||||||
|
$choices[] = ['value' => $code, 'label' => $name];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $choices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateOptions(array $options): array
|
||||||
|
{
|
||||||
|
$normalized = $options;
|
||||||
|
|
||||||
|
if (!($normalized['use_ai_sentence'] ?? false) && isset($normalized['sentence']) && empty(trim((string) $normalized['sentence']))) {
|
||||||
|
$normalized['sentence'] = self::DEFAULT_SENTENCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = parent::validateOptions($normalized);
|
||||||
|
|
||||||
|
if (!($validated['use_ai_sentence'] ?? false) && empty(trim((string) ($validated['sentence'] ?? '')))) {
|
||||||
|
$validated['sentence'] = self::DEFAULT_SENTENCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $validated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate passwords from phrases
|
||||||
|
*/
|
||||||
|
public function generate(int $count = 1, array $options = []): array
|
||||||
|
{
|
||||||
|
// Validate options
|
||||||
|
$options = $this->validateOptions($options);
|
||||||
|
|
||||||
|
// Validate count
|
||||||
|
if ($count < 1 || $count > 100) {
|
||||||
|
throw new InvalidArgumentException('Count must be between 1 and 100');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->logger && $this->isLogLevel('debug')) {
|
||||||
|
$this->logger->debug('PhraseAbbreviator: starting generation', [
|
||||||
|
'count' => $count,
|
||||||
|
'extraction_type' => $options['character_extraction'],
|
||||||
|
'use_ai' => $options['use_ai_sentence'] ?? false
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$passwords = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
try {
|
||||||
|
// Get sentence
|
||||||
|
if ($options['use_ai_sentence'] ?? false) {
|
||||||
|
if (!$this->llmManager) {
|
||||||
|
throw new RuntimeException('LLM manager not configured');
|
||||||
|
}
|
||||||
|
$sentence = $this->generateAiSentence($options);
|
||||||
|
} else {
|
||||||
|
$sentence = $options['sentence'];
|
||||||
|
if (empty($sentence)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Sentence is required when not using AI generation'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate sentence
|
||||||
|
$this->validateSentence($sentence, $options['use_ai_sentence'] ?? false);
|
||||||
|
|
||||||
|
// Replace number words with digits
|
||||||
|
$processedSentence = $this->replaceNumberWords($sentence, $options['language']);
|
||||||
|
|
||||||
|
// Generate password from sentence
|
||||||
|
$password = $this->generatePasswordFromSentence(
|
||||||
|
$processedSentence,
|
||||||
|
$options['character_extraction'],
|
||||||
|
$options['language']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add optional number
|
||||||
|
if ($options['add_number']) {
|
||||||
|
$password = $this->addRandomNumber(
|
||||||
|
$password,
|
||||||
|
$options['number_digits'],
|
||||||
|
$options['number_position']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$passwords[] = [
|
||||||
|
'password' => $password,
|
||||||
|
'method' => 'phrase_abbreviator',
|
||||||
|
'source_sentence' => $sentence,
|
||||||
|
'extraction_type' => $options['character_extraction']
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
if ($this->logger) {
|
||||||
|
$this->logger->error('PhraseAbbreviator: generation iteration failed', [
|
||||||
|
'iteration' => $i + 1,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->logger && $this->isLogLevel('info')) {
|
||||||
|
$this->logger->info('PhraseAbbreviator: generation complete', [
|
||||||
|
'count' => count($passwords)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $passwords;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate sentence according to rules
|
||||||
|
*/
|
||||||
|
private function validateSentence(string $sentence, bool $isAiGenerated): void
|
||||||
|
{
|
||||||
|
$minWords = $isAiGenerated ? 12 : 8;
|
||||||
|
|
||||||
|
if (strlen(trim($sentence)) === 0) {
|
||||||
|
throw new InvalidArgumentException('Sentence cannot be empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sentence[0] !== strtoupper($sentence[0])) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Sentence must start with a capital letter'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match('/[A-Z]/', $sentence)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Sentence must contain at least one capitalized letter'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$words = preg_split('/\s+/', trim($sentence), -1, PREG_SPLIT_NO_EMPTY);
|
||||||
|
$wordCount = count($words);
|
||||||
|
|
||||||
|
if ($wordCount < $minWords) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
"Sentence must contain at least {$minWords} words (found {$wordCount})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate sentence using LLM
|
||||||
|
*/
|
||||||
|
private function generateAiSentence(array $options): string
|
||||||
|
{
|
||||||
|
$provider = $options['ai_provider'] ?? null;
|
||||||
|
$model = $options['ai_model'] ?? null;
|
||||||
|
$prompt = $options['ai_prompt'] ?? 'Generate a creative, memorable sentence.';
|
||||||
|
|
||||||
|
// Ensure minimum 12 words
|
||||||
|
$fullPrompt = $prompt . " The sentence must contain at least 12 words.";
|
||||||
|
|
||||||
|
if ($this->logger && $this->isLogLevel('debug')) {
|
||||||
|
$this->logger->debug('PhraseAbbreviator: requesting AI-generated sentence', [
|
||||||
|
'provider' => $provider,
|
||||||
|
'model' => $model
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sentence = $this->llmManager->generateText(
|
||||||
|
$fullPrompt,
|
||||||
|
$provider,
|
||||||
|
['model' => $model]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clean up sentence
|
||||||
|
$sentence = trim($sentence);
|
||||||
|
$sentence = rtrim($sentence, '.');
|
||||||
|
$sentence = ucfirst($sentence);
|
||||||
|
|
||||||
|
if ($this->logger && $this->isLogLevel('debug')) {
|
||||||
|
$this->logger->debug('PhraseAbbreviator: AI sentence generated', [
|
||||||
|
'word_count' => count(preg_split('/\s+/', $sentence))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sentence;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
if ($this->logger) {
|
||||||
|
$this->logger->error('PhraseAbbreviator: AI generation failed', [
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Failed to generate sentence from AI: {$e->getMessage()}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate password from sentence
|
||||||
|
*/
|
||||||
|
private function generatePasswordFromSentence(
|
||||||
|
string $sentence,
|
||||||
|
string $extractionType,
|
||||||
|
string $language
|
||||||
|
): string {
|
||||||
|
// Split into words
|
||||||
|
$words = preg_split('/\s+/', trim($sentence), -1, PREG_SPLIT_NO_EMPTY);
|
||||||
|
|
||||||
|
$passwordChars = [];
|
||||||
|
|
||||||
|
foreach ($words as $word) {
|
||||||
|
// Remove leading/trailing punctuation but keep internal
|
||||||
|
$cleanWord = $word;
|
||||||
|
$leadingPunctuation = '';
|
||||||
|
$trailingPunctuation = '';
|
||||||
|
|
||||||
|
// Extract trailing punctuation
|
||||||
|
while (
|
||||||
|
strlen($cleanWord) > 0 &&
|
||||||
|
!ctype_alnum($cleanWord[strlen($cleanWord) - 1]) &&
|
||||||
|
$cleanWord[strlen($cleanWord) - 1] !== "'"
|
||||||
|
) {
|
||||||
|
$trailingPunctuation = substr($cleanWord, -1) . $trailingPunctuation;
|
||||||
|
$cleanWord = substr($cleanWord, 0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract leading punctuation
|
||||||
|
while (strlen($cleanWord) > 0 && !ctype_alnum($cleanWord[0])) {
|
||||||
|
$leadingPunctuation .= $cleanWord[0];
|
||||||
|
$cleanWord = substr($cleanWord, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen($cleanWord) === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract characters based on type
|
||||||
|
if ($extractionType === 'dual') {
|
||||||
|
$chars = $this->extractDualCharacters($cleanWord);
|
||||||
|
} else {
|
||||||
|
$chars = $cleanWord[0]; // Single character (first letter)
|
||||||
|
}
|
||||||
|
|
||||||
|
$passwordChars[] = $leadingPunctuation . $chars . $trailingPunctuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode('', $passwordChars);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract dual characters from word
|
||||||
|
*
|
||||||
|
* Word starts with vowel: first letter + first consonant
|
||||||
|
* Word starts with consonant: first letter + first vowel
|
||||||
|
* Fallback (no vowel/consonant found): first two letters
|
||||||
|
*/
|
||||||
|
private function extractDualCharacters(string $word): string
|
||||||
|
{
|
||||||
|
if (strlen($word) < 2) {
|
||||||
|
return $word;
|
||||||
|
}
|
||||||
|
|
||||||
|
$firstChar = $word[0];
|
||||||
|
|
||||||
|
if ($this->isVowel($firstChar)) {
|
||||||
|
// Word starts with vowel: first letter + first consonant
|
||||||
|
for ($i = 1; $i < strlen($word); $i++) {
|
||||||
|
if ($this->isConsonant($word[$i])) {
|
||||||
|
return $firstChar . $word[$i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No consonant found, return first two letters
|
||||||
|
return substr($word, 0, 2);
|
||||||
|
} else {
|
||||||
|
// Word starts with consonant: first letter + first vowel
|
||||||
|
for ($i = 1; $i < strlen($word); $i++) {
|
||||||
|
if ($this->isVowel($word[$i])) {
|
||||||
|
return $firstChar . $word[$i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No vowel found, return first two letters
|
||||||
|
return substr($word, 0, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if character is a vowel
|
||||||
|
*/
|
||||||
|
private function isVowel(string $char): bool
|
||||||
|
{
|
||||||
|
return strpos(self::VOWELS, $char) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if character is a consonant
|
||||||
|
*/
|
||||||
|
private function isConsonant(string $char): bool
|
||||||
|
{
|
||||||
|
return strpos(self::CONSONANTS, $char) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace number words using translations
|
||||||
|
*
|
||||||
|
* Loads number words from translation file and replaces all occurrences
|
||||||
|
* in the sentence with their digit equivalents
|
||||||
|
*/
|
||||||
|
private function replaceNumberWords(string $sentence, string $language): string
|
||||||
|
{
|
||||||
|
// Load number words from translation file
|
||||||
|
$numberWords = $this->loadNumberWordsFromTranslations($language);
|
||||||
|
|
||||||
|
if (empty($numberWords)) {
|
||||||
|
if ($this->logger && $this->isLogLevel('debug')) {
|
||||||
|
$this->logger->debug('PhraseAbbreviator: no number words found for language', [
|
||||||
|
'language' => $language
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return $sentence;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace each number word with its digit equivalent
|
||||||
|
foreach ($numberWords as $word => $digit) {
|
||||||
|
// Create regex pattern for word boundary matching
|
||||||
|
// This ensures 'one' doesn't match inside 'stone'
|
||||||
|
$pattern = '/\b' . preg_quote($word, '/') . '\b/i';
|
||||||
|
|
||||||
|
// Replace all occurrences (case-insensitive)
|
||||||
|
$sentence = preg_replace($pattern, $digit, $sentence);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sentence;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load number words from translation file
|
||||||
|
*
|
||||||
|
* Searches translation file for keys matching pattern: phraseabbreviator_number_{name}
|
||||||
|
* Where {name} is: zero, one, two, three... twenty
|
||||||
|
*/
|
||||||
|
private function loadNumberWordsFromTranslations(string $language): array
|
||||||
|
{
|
||||||
|
// Construct path to locale file
|
||||||
|
$localeDir = __DIR__ . '/../../locales';
|
||||||
|
$localeFile = "{$localeDir}/{$language}.json";
|
||||||
|
|
||||||
|
// Return empty array if file doesn't exist
|
||||||
|
if (!file_exists($localeFile)) {
|
||||||
|
if ($this->logger) {
|
||||||
|
$this->logger->warning('PhraseAbbreviator: translation file not found', [
|
||||||
|
'language' => $language,
|
||||||
|
'file' => $localeFile
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load JSON file
|
||||||
|
$content = file_get_contents($localeFile);
|
||||||
|
if ($content === false) {
|
||||||
|
if ($this->logger) {
|
||||||
|
$this->logger->warning('PhraseAbbreviator: failed to read translation file', [
|
||||||
|
'file' => $localeFile
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode JSON
|
||||||
|
$translations = json_decode($content, true);
|
||||||
|
if ($translations === null || !is_array($translations)) {
|
||||||
|
if ($this->logger) {
|
||||||
|
$this->logger->warning('PhraseAbbreviator: failed to decode translation file', [
|
||||||
|
'file' => $localeFile
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract number words
|
||||||
|
$numberWords = [];
|
||||||
|
$prefix = 'phraseabbreviator_number_';
|
||||||
|
|
||||||
|
foreach ($translations as $key => $value) {
|
||||||
|
// Check if key starts with prefix
|
||||||
|
if (str_starts_with($key, $prefix)) {
|
||||||
|
// Extract digit name from key
|
||||||
|
// Example: 'phraseabbreviator_number_one' -> 'one'
|
||||||
|
$digitName = substr($key, strlen($prefix));
|
||||||
|
|
||||||
|
// Convert digit name to actual digit
|
||||||
|
$digit = $this->digitFromKeyName($digitName);
|
||||||
|
|
||||||
|
// Add to mapping if digit was successfully converted
|
||||||
|
if ($digit !== null) {
|
||||||
|
$numberWords[$value] = $digit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->logger && $this->isLogLevel('debug')) {
|
||||||
|
$this->logger->debug('PhraseAbbreviator: loaded number words', [
|
||||||
|
'language' => $language,
|
||||||
|
'count' => count($numberWords)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $numberWords;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert digit key name to digit string
|
||||||
|
*
|
||||||
|
* Maps word names to their numeric equivalents
|
||||||
|
*/
|
||||||
|
private function digitFromKeyName(string $keyName): ?string
|
||||||
|
{
|
||||||
|
// Map of key names to digits
|
||||||
|
$digitMap = [
|
||||||
|
'zero' => '0',
|
||||||
|
'one' => '1',
|
||||||
|
'two' => '2',
|
||||||
|
'three' => '3',
|
||||||
|
'four' => '4',
|
||||||
|
'five' => '5',
|
||||||
|
'six' => '6',
|
||||||
|
'seven' => '7',
|
||||||
|
'eight' => '8',
|
||||||
|
'nine' => '9',
|
||||||
|
'ten' => '10',
|
||||||
|
'eleven' => '11',
|
||||||
|
'twelve' => '12',
|
||||||
|
'thirteen' => '13',
|
||||||
|
'fourteen' => '14',
|
||||||
|
'fifteen' => '15',
|
||||||
|
'sixteen' => '16',
|
||||||
|
'seventeen' => '17',
|
||||||
|
'eighteen' => '18',
|
||||||
|
'nineteen' => '19',
|
||||||
|
'twenty' => '20'
|
||||||
|
];
|
||||||
|
|
||||||
|
return $digitMap[$keyName] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add random number to password
|
||||||
|
*/
|
||||||
|
private function addRandomNumber(
|
||||||
|
string $password,
|
||||||
|
int $digits,
|
||||||
|
string $position
|
||||||
|
): string {
|
||||||
|
$max = (int)str_repeat('9', $digits);
|
||||||
|
$number = random_int(0, $max);
|
||||||
|
$paddedNumber = str_pad((string)$number, $digits, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
if ($position === 'prepend') {
|
||||||
|
return $paddedNumber . $password;
|
||||||
|
} else {
|
||||||
|
return $password . $paddedNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,621 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Generators;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use App\Services\Logging\LoggingHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PWGen Enhanced Generator v3.2
|
||||||
|
*
|
||||||
|
* Generates pronounceable passwords with intelligent syllable-based separation
|
||||||
|
* based on phonetic patterns (VC, CV, VCC, CVC, VVC, CVV).
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Native pwgen executable support with comprehensive options
|
||||||
|
* - PHP fallback with phonetic syllable separation using PwGenPhpNg
|
||||||
|
* - Intelligent syllable splitting based on vowel/consonant patterns
|
||||||
|
* - Random separator selection for EACH separator position
|
||||||
|
* - Automatic number placement at syllable endings
|
||||||
|
* - No leading/trailing separators
|
||||||
|
*
|
||||||
|
* @package App\Generators
|
||||||
|
*/
|
||||||
|
class PwgenGenerator extends AbstractPasswordGenerator
|
||||||
|
{
|
||||||
|
private const GENERATOR_ID = 'pwgen';
|
||||||
|
private const MIN_SYLLABLE_LENGTH = 2;
|
||||||
|
private const MAX_SYLLABLE_LENGTH = 4;
|
||||||
|
private const VOWELS = 'aeiouAEIOU';
|
||||||
|
|
||||||
|
private ?object $pwgenFallback = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor with optional logger
|
||||||
|
*/
|
||||||
|
public function __construct(?LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
parent::__construct($logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return self::GENERATOR_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVersion(): string
|
||||||
|
{
|
||||||
|
return '2.1.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return 'PWGen Enhanced';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Generates pronounceable passwords with intelligent syllable-based separation using phonetic patterns.';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get option definitions
|
||||||
|
*/
|
||||||
|
public function getOptionDefinitions(): array
|
||||||
|
{
|
||||||
|
$options = [
|
||||||
|
'length' => [
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => 8,
|
||||||
|
'min' => 4,
|
||||||
|
'max' => 32,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'basic',
|
||||||
|
'description' => 'Total length of the generated password'
|
||||||
|
],
|
||||||
|
'separator' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => '',
|
||||||
|
'maxLength' => 10,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'advanced',
|
||||||
|
'description' => 'Characters to use as separators between syllables. If multiple characters are provided, one will be chosen at random for each separator position.'
|
||||||
|
],
|
||||||
|
'include_numbers' => [
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'character_set',
|
||||||
|
'description' => 'Whether to include numbers in the generated password'
|
||||||
|
],
|
||||||
|
'include_capitals' => [
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'character_set',
|
||||||
|
'description' => 'Whether to include capital letters in the generated password'
|
||||||
|
],
|
||||||
|
'secure_mode' => [
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => false,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'advanced',
|
||||||
|
'description' => 'Whether to use secure mode (fully random) generation'
|
||||||
|
],
|
||||||
|
'no_ambiguous' => [
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => false,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'advanced',
|
||||||
|
'description' => 'Whether to exclude ambiguous characters (like 0/O, 1/l, etc.) from the generated password'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateOptions(array $options): array
|
||||||
|
{
|
||||||
|
return parent::validateOptions($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate passwords
|
||||||
|
*/
|
||||||
|
public function generate(int $count = 1, array $options = []): array
|
||||||
|
{
|
||||||
|
// Validate options using parent class method
|
||||||
|
$options = $this->validateOptions($options);
|
||||||
|
$length = $options['length'];
|
||||||
|
$separator = $options['separator'];
|
||||||
|
|
||||||
|
if ($this->logger && $this->isLogLevel('debug')) {
|
||||||
|
$this->logger->debug('Pwgen: starting generation', [
|
||||||
|
'count' => $count,
|
||||||
|
'length' => $length,
|
||||||
|
'has_separator' => !empty($separator)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
LoggingHelper::debug($this->logger, $this->getId(), 'Message');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// If separator is provided, use syllable splitting; otherwise use standard generation
|
||||||
|
if (!empty($separator)) {
|
||||||
|
return $this->generateWithSyllableSeparation($length, $count, $separator, $options);
|
||||||
|
} else {
|
||||||
|
// Use native pwgen or PHP fallback without separation
|
||||||
|
if ($this->isPwgenCommandAvailable()) {
|
||||||
|
return $this->generateWithNativePwgen($length, $count, $options);
|
||||||
|
} else {
|
||||||
|
return $this->generateWithPhpFallback($length, $count, $options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
if ($this->logger) {
|
||||||
|
$this->logger->error('Pwgen: generation failed', [
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
throw new RuntimeException(
|
||||||
|
"PWGen password generation failed: " . $e->getMessage(),
|
||||||
|
0,
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate passwords using native pwgen executable
|
||||||
|
*/
|
||||||
|
private function generateWithNativePwgen(int $length, int $count, array $options): array
|
||||||
|
{
|
||||||
|
$command = ['pwgen'];
|
||||||
|
|
||||||
|
// Add pwgen flags based on options
|
||||||
|
if ($options['include_capitals']) {
|
||||||
|
$command[] = '-c'; // Include capital letters
|
||||||
|
} else {
|
||||||
|
$command[] = '-A'; // No capitals
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($options['include_numbers']) {
|
||||||
|
$command[] = '-n'; // Include numbers
|
||||||
|
} else {
|
||||||
|
$command[] = '-0'; // No numbers
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($options['secure_mode']) {
|
||||||
|
$command[] = '-s'; // Secure (fully random)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($options['no_ambiguous']) {
|
||||||
|
$command[] = '-B'; // No ambiguous characters
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add length and count
|
||||||
|
$command[] = (string)$length;
|
||||||
|
$command[] = (string)$count;
|
||||||
|
|
||||||
|
// Execute command
|
||||||
|
$result = shell_exec(escapeshellcmd(implode(' ', $command)));
|
||||||
|
|
||||||
|
if ($result === null || $result === '') {
|
||||||
|
throw new RuntimeException('PWGen command execution failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
$passwords = array_filter(explode("\n", trim($result)));
|
||||||
|
|
||||||
|
if ($this->logger && $this->isLogLevel('debug')) {
|
||||||
|
$this->logger->debug('Pwgen: native pwgen generated', ['count' => count($passwords)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return array of password objects
|
||||||
|
return array_map(function ($password) {
|
||||||
|
return [
|
||||||
|
'password' => $password,
|
||||||
|
'method' => 'native_pwgen'
|
||||||
|
];
|
||||||
|
}, $passwords);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate passwords with PHP fallback (no separation)
|
||||||
|
*/
|
||||||
|
private function generateWithPhpFallback(int $length, int $count, array $options): array
|
||||||
|
{
|
||||||
|
$this->initializePwgenFallback();
|
||||||
|
$passwords = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
// Generate password using PHP fallback
|
||||||
|
$generatedPasswords = $this->pwgenFallback->generate(
|
||||||
|
$length,
|
||||||
|
1,
|
||||||
|
$options['include_numbers'],
|
||||||
|
$options['include_capitals']
|
||||||
|
);
|
||||||
|
|
||||||
|
$passwords[] = [
|
||||||
|
'password' => $generatedPasswords[0],
|
||||||
|
'method' => 'php_fallback'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->logger && $this->isLogLevel('debug')) {
|
||||||
|
$this->logger->debug('Pwgen: PHP fallback generated', ['count' => count($passwords)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $passwords;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate passwords with intelligent syllable separation
|
||||||
|
*/
|
||||||
|
private function generateWithSyllableSeparation(int $length, int $count, string $separator, array $options): array
|
||||||
|
{
|
||||||
|
$passwords = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
// Generate base password first (without separator)
|
||||||
|
$baseLength = $length;
|
||||||
|
|
||||||
|
if ($this->isPwgenCommandAvailable() && !$options['secure_mode']) {
|
||||||
|
// Use native pwgen for base password
|
||||||
|
$result = $this->generateWithNativePwgen($baseLength, 1, $options);
|
||||||
|
$basePassword = trim($result[0]['password']);
|
||||||
|
} else {
|
||||||
|
// Use PHP fallback
|
||||||
|
$this->initializePwgenFallback();
|
||||||
|
$generated = $this->pwgenFallback->generate(
|
||||||
|
$baseLength,
|
||||||
|
1,
|
||||||
|
$options['include_numbers'],
|
||||||
|
$options['include_capitals']
|
||||||
|
);
|
||||||
|
$basePassword = $generated[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split into phonetic syllables
|
||||||
|
$syllables = $this->splitIntoPhoneticSyllables($basePassword);
|
||||||
|
|
||||||
|
// Join syllables with random separator selection for each position
|
||||||
|
$finalPassword = $this->joinSyllablesWithRandomSeparators($syllables, $separator);
|
||||||
|
|
||||||
|
// Trim to target length if needed (but preserve complete syllables when possible)
|
||||||
|
if (strlen($finalPassword) > $length) {
|
||||||
|
$finalPassword = $this->trimToLength($finalPassword, $length, $separator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract used separators for display
|
||||||
|
$usedSeparators = $this->extractUsedSeparators($finalPassword, $syllables);
|
||||||
|
|
||||||
|
$passwords[] = [
|
||||||
|
'password' => $finalPassword,
|
||||||
|
'syllables' => $syllables,
|
||||||
|
'separator_used' => implode('', array_unique($usedSeparators)),
|
||||||
|
'syllable_count' => count($syllables),
|
||||||
|
'method' => 'syllable_separated'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->logger && $this->isLogLevel('info')) {
|
||||||
|
$this->logger->info('Pwgen: syllable-separated passwords generated', [
|
||||||
|
'count' => count($passwords),
|
||||||
|
'separator' => $separator
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $passwords;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Join syllables with random separator selection for EACH separator position
|
||||||
|
*
|
||||||
|
* Ensures no leading or trailing separators
|
||||||
|
*/
|
||||||
|
private function joinSyllablesWithRandomSeparators(array $syllables, string $separatorChars): string
|
||||||
|
{
|
||||||
|
if (count($syllables) === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($syllables) === 1) {
|
||||||
|
return $syllables[0]; // No separator needed for single syllable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get available separator characters (excluding spaces)
|
||||||
|
$availableChars = str_split($separatorChars);
|
||||||
|
$availableChars = array_filter($availableChars, fn($char) => $char !== ' ');
|
||||||
|
|
||||||
|
if (empty($availableChars)) {
|
||||||
|
// Fallback to dash if no valid separators
|
||||||
|
$availableChars = ['-'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = '';
|
||||||
|
$syllableCount = count($syllables);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $syllableCount; $i++) {
|
||||||
|
// Add the syllable
|
||||||
|
$result .= $syllables[$i];
|
||||||
|
|
||||||
|
// Add random separator BETWEEN syllables (not after last)
|
||||||
|
if ($i < $syllableCount - 1) {
|
||||||
|
// Select random separator for THIS position
|
||||||
|
$randomSeparator = $availableChars[array_rand($availableChars)];
|
||||||
|
$result .= $randomSeparator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trim password to target length while preserving structure
|
||||||
|
*/
|
||||||
|
private function trimToLength(string $password, int $targetLength, string $separatorChars): string
|
||||||
|
{
|
||||||
|
if (strlen($password) <= $targetLength) {
|
||||||
|
return $password;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple trim, ensuring no trailing separator
|
||||||
|
$trimmed = substr($password, 0, $targetLength);
|
||||||
|
|
||||||
|
// Remove trailing separator if present
|
||||||
|
$separatorArray = str_split($separatorChars);
|
||||||
|
$separatorArray = array_filter($separatorArray, fn($char) => $char !== ' ');
|
||||||
|
|
||||||
|
while (strlen($trimmed) > 0 && in_array(substr($trimmed, -1), $separatorArray)) {
|
||||||
|
$trimmed = substr($trimmed, 0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract separators actually used in the password
|
||||||
|
*/
|
||||||
|
private function extractUsedSeparators(string $password, array $syllables): array
|
||||||
|
{
|
||||||
|
$used = [];
|
||||||
|
// Simple extraction: find chars that aren't in any syllable
|
||||||
|
$syllableChars = implode('', $syllables);
|
||||||
|
|
||||||
|
foreach (str_split($password) as $char) {
|
||||||
|
if (strpos($syllableChars, $char) === false && !in_array($char, $used)) {
|
||||||
|
$used[] = $char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $used;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split password into phonetic syllables based on vowel/consonant patterns
|
||||||
|
*
|
||||||
|
* Preferred patterns (longer syllables first):
|
||||||
|
* - CVV (Consonant-Vowel-Vowel)
|
||||||
|
* - VVC (Vowel-Vowel-Consonant)
|
||||||
|
* - CVC (Consonant-Vowel-Consonant)
|
||||||
|
* - VCC (Vowel-Consonant-Consonant)
|
||||||
|
* - CV (Consonant-Vowel)
|
||||||
|
* - VC (Vowel-Consonant)
|
||||||
|
* - Numbers preferably at syllable end
|
||||||
|
*/
|
||||||
|
private function splitIntoPhoneticSyllables(string $password): array
|
||||||
|
{
|
||||||
|
$syllables = [];
|
||||||
|
$currentSyllable = '';
|
||||||
|
$chars = str_split($password);
|
||||||
|
$length = count($chars);
|
||||||
|
$i = 0;
|
||||||
|
|
||||||
|
while ($i < $length) {
|
||||||
|
$char = $chars[$i];
|
||||||
|
$currentSyllable .= $char;
|
||||||
|
|
||||||
|
// Check if current syllable should end
|
||||||
|
$shouldEnd = false;
|
||||||
|
$syllableLen = strlen($currentSyllable);
|
||||||
|
|
||||||
|
// Minimum syllable length check
|
||||||
|
if ($syllableLen >= self::MIN_SYLLABLE_LENGTH) {
|
||||||
|
// Look ahead to decide
|
||||||
|
$pattern = $this->getPhoneticPattern($currentSyllable);
|
||||||
|
$nextChar = ($i + 1 < $length) ? $chars[$i + 1] : null;
|
||||||
|
|
||||||
|
// End syllable if:
|
||||||
|
// 1. We have a complete pattern (CVV, CVC, etc.)
|
||||||
|
// 2. Maximum length reached
|
||||||
|
// 3. Number at end of syllable
|
||||||
|
// 4. Good breaking point
|
||||||
|
|
||||||
|
if ($syllableLen >= self::MAX_SYLLABLE_LENGTH) {
|
||||||
|
$shouldEnd = true;
|
||||||
|
} elseif ($this->isNumber($char) && $nextChar && !$this->isNumber($nextChar)) {
|
||||||
|
// Number at end, good break point
|
||||||
|
$shouldEnd = true;
|
||||||
|
} elseif ($this->isCompletePattern($pattern) && $syllableLen >= 3) {
|
||||||
|
// Have a good 3+ char pattern
|
||||||
|
$shouldEnd = true;
|
||||||
|
} elseif ($syllableLen >= 2 && $nextChar === null) {
|
||||||
|
// End of password
|
||||||
|
$shouldEnd = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($shouldEnd && strlen($currentSyllable) >= self::MIN_SYLLABLE_LENGTH) {
|
||||||
|
$syllables[] = $currentSyllable;
|
||||||
|
$currentSyllable = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add remaining characters
|
||||||
|
if (!empty($currentSyllable)) {
|
||||||
|
if (count($syllables) > 0 && strlen($currentSyllable) < self::MIN_SYLLABLE_LENGTH) {
|
||||||
|
// Merge short remainder with last syllable
|
||||||
|
$syllables[count($syllables) - 1] .= $currentSyllable;
|
||||||
|
} else {
|
||||||
|
$syllables[] = $currentSyllable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $syllables;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get phonetic pattern of a string (V=vowel, C=consonant, N=number)
|
||||||
|
*/
|
||||||
|
private function getPhoneticPattern(string $str): string
|
||||||
|
{
|
||||||
|
$pattern = '';
|
||||||
|
|
||||||
|
foreach (str_split($str) as $char) {
|
||||||
|
if ($this->isNumber($char)) {
|
||||||
|
$pattern .= 'N';
|
||||||
|
} elseif ($this->isVowel($char)) {
|
||||||
|
$pattern .= 'V';
|
||||||
|
} else {
|
||||||
|
$pattern .= 'C';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if pattern is complete/good for syllable breaking
|
||||||
|
*/
|
||||||
|
private function isCompletePattern(string $pattern): bool
|
||||||
|
{
|
||||||
|
$goodPatterns = ['CVV', 'VVC', 'CVC', 'VCC', 'CVN', 'VCN', 'CVCN'];
|
||||||
|
return in_array($pattern, $goodPatterns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if character is a vowel
|
||||||
|
*/
|
||||||
|
private function isVowel(string $char): bool
|
||||||
|
{
|
||||||
|
return strpos(self::VOWELS, $char) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if character is a number
|
||||||
|
*/
|
||||||
|
private function isNumber(string $char): bool
|
||||||
|
{
|
||||||
|
return is_numeric($char);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if native pwgen command is available
|
||||||
|
*/
|
||||||
|
private function isPwgenCommandAvailable(): bool
|
||||||
|
{
|
||||||
|
static $available = null;
|
||||||
|
|
||||||
|
if ($available === null) {
|
||||||
|
$which = shell_exec('which pwgen 2>/dev/null');
|
||||||
|
$available = !empty($which) && is_executable(trim($which));
|
||||||
|
|
||||||
|
// Alternative check for Windows
|
||||||
|
if (!$available && PHP_OS_FAMILY === 'Windows') {
|
||||||
|
$where = shell_exec('where pwgen 2>nul');
|
||||||
|
$available = !empty($where);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $available;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize PHP fallback if not already done
|
||||||
|
*
|
||||||
|
* Uses the PwGenPhpNg class at src/Generators/Classes/PwGenPhpNg.class.php
|
||||||
|
*/
|
||||||
|
private function initializePwgenFallback(): void
|
||||||
|
{
|
||||||
|
if ($this->pwgenFallback === null) {
|
||||||
|
require_once __DIR__ . '/Classes/PwGenPhpNg.class.php';
|
||||||
|
$this->pwgenFallback = new \PwGenPhpNg();
|
||||||
|
|
||||||
|
if ($this->logger && $this->isLogLevel('debug')) {
|
||||||
|
$this->logger->debug('Pwgen: PHP fallback initialized');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate entropy estimate for current PWGen option set.
|
||||||
|
*/
|
||||||
|
public function calculateEntropyForOptions(array $options): float
|
||||||
|
{
|
||||||
|
$breakdown = $this->getEntropyBreakdownForOptions($options);
|
||||||
|
return (float) ($breakdown['entropy_bits'] ?? 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detailed entropy contributors for API transparency.
|
||||||
|
*/
|
||||||
|
public function getEntropyBreakdownForOptions(array $options): array
|
||||||
|
{
|
||||||
|
$validated = $this->validateOptions($options);
|
||||||
|
|
||||||
|
$length = max(1, (int) ($validated['length'] ?? 8));
|
||||||
|
$secureMode = (bool) ($validated['secure_mode'] ?? false);
|
||||||
|
$includeNumbers = (bool) ($validated['include_numbers'] ?? true);
|
||||||
|
$includeCapitals = (bool) ($validated['include_capitals'] ?? true);
|
||||||
|
$noAmbiguous = (bool) ($validated['no_ambiguous'] ?? false);
|
||||||
|
$separatorChars = str_replace(' ', '', (string) ($validated['separator'] ?? ''));
|
||||||
|
|
||||||
|
$charsetSize = 26; // lowercase always available
|
||||||
|
if ($includeCapitals) {
|
||||||
|
$charsetSize += 26;
|
||||||
|
}
|
||||||
|
if ($includeNumbers) {
|
||||||
|
$charsetSize += 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
$symbolEntropyBits = 0.0;
|
||||||
|
if ($separatorChars !== '') {
|
||||||
|
$separatorAlphabet = count(array_unique(str_split($separatorChars)));
|
||||||
|
$separatorSlotsEstimate = max(1, (int) floor($length / 3));
|
||||||
|
if ($separatorAlphabet > 1) {
|
||||||
|
$symbolEntropyBits = $separatorSlotsEstimate * log((float) $separatorAlphabet, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($noAmbiguous) {
|
||||||
|
// Approximate reduced alphabet due to ambiguity filtering.
|
||||||
|
$charsetSize = max(24, (int) floor($charsetSize * 0.85));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secure mode approximates random selection over active charset.
|
||||||
|
$baseEntropyBits = $length * log((float) $charsetSize, 2);
|
||||||
|
|
||||||
|
// Pronounceable mode has structure constraints; apply conservative penalty.
|
||||||
|
$structurePenaltyBits = $secureMode ? 0.0 : (0.35 * $length);
|
||||||
|
$totalEntropyBits = max(0.0, $baseEntropyBits - $structurePenaltyBits + $symbolEntropyBits);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'entropy_model' => 'pwgen-structured-heuristic-v1',
|
||||||
|
'model_confidence' => $secureMode ? 'medium' : 'low',
|
||||||
|
'length' => $length,
|
||||||
|
'secure_mode' => $secureMode,
|
||||||
|
'include_numbers' => $includeNumbers,
|
||||||
|
'include_capitals' => $includeCapitals,
|
||||||
|
'no_ambiguous' => $noAmbiguous,
|
||||||
|
'estimated_charset_size' => $charsetSize,
|
||||||
|
'base_entropy_bits' => round($baseEntropyBits, 1),
|
||||||
|
'structure_penalty_bits' => round($structurePenaltyBits, 1),
|
||||||
|
'separator_entropy_bits' => round($symbolEntropyBits, 1),
|
||||||
|
'entropy_bits' => round($totalEntropyBits, 1),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,878 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Generators;
|
||||||
|
|
||||||
|
use App\Services\LLM\LLMManager;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XKCD Pass Generator v2.4
|
||||||
|
*
|
||||||
|
* Generates memorable passphrases from curated wordlists.
|
||||||
|
* Supports traditional wordlist selection or AI-generated custom wordlists via LLM.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Multiple language wordlists
|
||||||
|
* - Intelligent word filtering (length, spaces)
|
||||||
|
* - Random separator selection for each position
|
||||||
|
* - Optional number insertion
|
||||||
|
* - Entropy calculation
|
||||||
|
* - AI-powered custom wordlist generation
|
||||||
|
*
|
||||||
|
* @package App\Generators
|
||||||
|
*/
|
||||||
|
class XkcdPassGenerator extends AbstractPasswordGenerator
|
||||||
|
{
|
||||||
|
private const GENERATOR_ID = 'xkcdpass';
|
||||||
|
private const DEFAULT_WORDLISTS_DIR = 'data/wordlists';
|
||||||
|
|
||||||
|
private array $wordlists = [];
|
||||||
|
private array $availableWordlists = [];
|
||||||
|
private array $wordlistFileMap = [];
|
||||||
|
private ?LLMManager $llmManager = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor with optional LLM Manager and logger
|
||||||
|
*/
|
||||||
|
|
||||||
|
public function __construct(?LLMManager $llmManager = null, ?LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
parent::__construct($logger);
|
||||||
|
$this->llmManager = $llmManager;
|
||||||
|
$this->initializeWordlists();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return self::GENERATOR_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVersion(): string
|
||||||
|
{
|
||||||
|
return '2.4.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return 'XKCD Pass';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Memorable passphrases using word combinations with enhanced filtering and separator options';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAvailableLanguages(): array
|
||||||
|
{
|
||||||
|
$languages = [];
|
||||||
|
foreach ($this->availableWordlists as $code => $label) {
|
||||||
|
$filename = $this->getWordlistFilename($code);
|
||||||
|
$path = $this->getWordlistPath() . '/' . $filename;
|
||||||
|
|
||||||
|
$languages[$code] = [
|
||||||
|
'name' => $label,
|
||||||
|
'description' => 'Wordlist for ' . $label,
|
||||||
|
'file' => $filename,
|
||||||
|
'available' => file_exists($path) && is_readable($path),
|
||||||
|
'fallback' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $languages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get option definitions with AI wordlist support
|
||||||
|
*/
|
||||||
|
public function getOptionDefinitions(): array
|
||||||
|
{
|
||||||
|
$options = [
|
||||||
|
'words' => [
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => 4,
|
||||||
|
'min' => 2,
|
||||||
|
'max' => 8,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'basic',
|
||||||
|
'description' => 'Number of words to include in the passphrase'
|
||||||
|
],
|
||||||
|
'wordlist' => [
|
||||||
|
'type' => 'select',
|
||||||
|
'default' => 'en',
|
||||||
|
'choices' => $this->getAvailableWordlistChoices(),
|
||||||
|
'translateChoices' => false,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'basic',
|
||||||
|
'description' => 'Wordlist to use for generating the passphrase'
|
||||||
|
],
|
||||||
|
'separator' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => '-',
|
||||||
|
'maxLength' => 5,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'basic',
|
||||||
|
'description' => 'Separator character(s) between words. If multiple, one is randomly selected for each position'
|
||||||
|
],
|
||||||
|
'max_word_length' => [
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => 0,
|
||||||
|
'min' => 0,
|
||||||
|
'max' => 30,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'advanced',
|
||||||
|
'description' => 'Maximum length for words (0 = no limit)'
|
||||||
|
],
|
||||||
|
'space_handling' => [
|
||||||
|
'type' => 'select',
|
||||||
|
'default' => 'allow',
|
||||||
|
'choices' => [
|
||||||
|
['value' => 'allow', 'label' => 'Allow'],
|
||||||
|
['value' => 'skip', 'label' => 'Skip'],
|
||||||
|
['value' => 'modify', 'label' => 'Modify']
|
||||||
|
],
|
||||||
|
'translateChoices' => true,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'advanced',
|
||||||
|
'description' => 'How to handle words containing spaces: allow as-is, skip them, or modify to underscores'
|
||||||
|
],
|
||||||
|
'add_number' => [
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => false,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'advanced',
|
||||||
|
'description' => 'Add random number to the passphrase'
|
||||||
|
],
|
||||||
|
'number_position' => [
|
||||||
|
'type' => 'select',
|
||||||
|
'default' => 'end',
|
||||||
|
'choices' => [
|
||||||
|
['value' => 'beginning', 'label' => 'Beginning'],
|
||||||
|
['value' => 'end', 'label' => 'End'],
|
||||||
|
['value' => 'random', 'label' => 'Random']
|
||||||
|
],
|
||||||
|
'translateChoices' => true,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'advanced',
|
||||||
|
'description' => 'Where to place the random number'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add AI wordlist generation options if LLM available
|
||||||
|
if ($this->llmManager && $this->llmManager->hasAvailableProvider()) {
|
||||||
|
$options['use_ai_wordlist'] = [
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => false,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'ai',
|
||||||
|
'description' => 'Generate custom wordlist using AI'
|
||||||
|
];
|
||||||
|
|
||||||
|
$options['ai_wordlist_prompt'] = [
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => '',
|
||||||
|
'maxLength' => 500,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'ai',
|
||||||
|
'description' => 'Complete the prompt: "Create a list of X words of at least 5 characters, which..."'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Get available LLM providers
|
||||||
|
$providers = $this->llmManager->getAvailableProviders();
|
||||||
|
if (!empty($providers)) {
|
||||||
|
$providerChoices = [];
|
||||||
|
foreach ($providers as $name => $info) {
|
||||||
|
$providerChoices[] = [
|
||||||
|
'value' => $name,
|
||||||
|
'label' => $info['display_name']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$options['ai_provider'] = [
|
||||||
|
'type' => 'select',
|
||||||
|
'default' => $this->llmManager->getDefaultProviderName(),
|
||||||
|
'choices' => $providerChoices,
|
||||||
|
'translateChoices' => false,
|
||||||
|
'required' => false,
|
||||||
|
'group' => 'ai',
|
||||||
|
'description' => 'Which LLM provider to use'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get wordlist choices for UI
|
||||||
|
*/
|
||||||
|
private function getAvailableWordlistChoices(): array
|
||||||
|
{
|
||||||
|
$choices = [];
|
||||||
|
foreach ($this->availableWordlists as $code => $name) {
|
||||||
|
$choices[] = [
|
||||||
|
'value' => $code,
|
||||||
|
'label' => $name
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $choices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateOptions(array $options): array
|
||||||
|
{
|
||||||
|
$normalized = $options;
|
||||||
|
|
||||||
|
if (isset($normalized['wordlist']) && is_string($normalized['wordlist']) && trim($normalized['wordlist']) === '') {
|
||||||
|
$normalized['wordlist'] = 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = parent::validateOptions($normalized);
|
||||||
|
|
||||||
|
if (empty(trim((string) ($validated['wordlist'] ?? '')))) {
|
||||||
|
$validated['wordlist'] = 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $validated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate passphrases
|
||||||
|
*/
|
||||||
|
public function generate(int $count = 1, array $options = []): array
|
||||||
|
{
|
||||||
|
// Validate options using parent class method
|
||||||
|
$options = $this->validateOptions($options);
|
||||||
|
|
||||||
|
if ($count < 1 || $count > 100) {
|
||||||
|
throw new InvalidArgumentException('Count must be between 1 and 100');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->logger && $this->isLogLevel('debug')) {
|
||||||
|
$this->logger->debug('XkcdPass: starting generation', [
|
||||||
|
'count' => $count,
|
||||||
|
'use_ai' => $options['use_ai_wordlist'] ?? false
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
try {
|
||||||
|
// Check if using AI-generated wordlist
|
||||||
|
if ($options['use_ai_wordlist'] ?? false) {
|
||||||
|
if (!$this->llmManager) {
|
||||||
|
throw new RuntimeException('LLM manager not configured for AI wordlist generation');
|
||||||
|
}
|
||||||
|
|
||||||
|
$wordlist = $this->generateAiWordlist(
|
||||||
|
$options['words'],
|
||||||
|
$options['ai_wordlist_prompt'] ?? '',
|
||||||
|
$options['ai_provider'] ?? null
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$wordlist = $this->getWordlist($options['wordlist']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$results[] = $this->generateSingle($wordlist, $options);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
if ($this->logger) {
|
||||||
|
$this->logger->error('XkcdPass: generation iteration failed', [
|
||||||
|
'iteration' => $i + 1,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->logger && $this->isLogLevel('info')) {
|
||||||
|
$this->logger->info('XkcdPass: generation complete', [
|
||||||
|
'count' => count($results)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate wordlist using LLM
|
||||||
|
*
|
||||||
|
* Calculates total words needed: count × words_per_password
|
||||||
|
* Sends request to LLM to generate that many words
|
||||||
|
*/
|
||||||
|
private function generateAiWordlist(
|
||||||
|
int $wordCount,
|
||||||
|
string $userPrompt,
|
||||||
|
?string $provider
|
||||||
|
): array {
|
||||||
|
// Build complete prompt
|
||||||
|
$basePrompt = "Create a list of $wordCount words of at least 5 characters";
|
||||||
|
|
||||||
|
if (!empty($userPrompt)) {
|
||||||
|
// User completes: "which..."
|
||||||
|
$fullPrompt = $basePrompt . ", which " . trim($userPrompt);
|
||||||
|
} else {
|
||||||
|
$fullPrompt = $basePrompt . ", which are memorable and diverse";
|
||||||
|
}
|
||||||
|
|
||||||
|
$fullPrompt .= ". Return ONLY the words, one per line, without numbers or any formatting.";
|
||||||
|
|
||||||
|
if ($this->logger && $this->isLogLevel('debug')) {
|
||||||
|
$this->logger->debug('XkcdPass: requesting AI wordlist', [
|
||||||
|
'word_count' => $wordCount,
|
||||||
|
'provider' => $provider
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call LLM
|
||||||
|
$response = $this->llmManager->generateText($fullPrompt, $provider);
|
||||||
|
|
||||||
|
// Parse response
|
||||||
|
$words = $this->parseWordList($response, $wordCount);
|
||||||
|
|
||||||
|
if (count($words) < $wordCount) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"LLM returned only " . count($words) . " words (expected $wordCount)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to wordlist format
|
||||||
|
$wordlist = [];
|
||||||
|
foreach (array_slice($words, 0, $wordCount) as $word) {
|
||||||
|
$wordlist[] = [
|
||||||
|
'word' => strtolower($word),
|
||||||
|
'explanation' => 'AI-generated'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->logger && $this->isLogLevel('debug')) {
|
||||||
|
$this->logger->debug('XkcdPass: AI wordlist generated', [
|
||||||
|
'count' => count($wordlist)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $wordlist;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
if ($this->logger) {
|
||||||
|
$this->logger->error('XkcdPass: AI wordlist generation failed', [
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Failed to generate wordlist with LLM: {$e->getMessage()}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse LLM response into clean word list
|
||||||
|
*/
|
||||||
|
private function parseWordList(string $response, int $expectedCount): array
|
||||||
|
{
|
||||||
|
$lines = preg_split('/[\r\n]+/', trim($response));
|
||||||
|
$words = [];
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (count($words) >= $expectedCount) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$line = trim($line);
|
||||||
|
|
||||||
|
// Skip empty lines
|
||||||
|
if (empty($line)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove leading numbers/bullets (e.g., "1. word", "- word")
|
||||||
|
$line = preg_replace('/^[\d\-\.\)\s]+/', '', $line);
|
||||||
|
$line = trim($line);
|
||||||
|
|
||||||
|
// Extract first word only
|
||||||
|
$parts = preg_split('/\s+/', $line);
|
||||||
|
$word = strtolower(trim($parts[0]));
|
||||||
|
|
||||||
|
// Validate word: at least 5 chars, only alphabetic
|
||||||
|
if (strlen($word) >= 5 && preg_match('/^[a-z]+$/', $word)) {
|
||||||
|
$words[] = $word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $words;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate single passphrase
|
||||||
|
*/
|
||||||
|
private function generateSingle(array $wordlist, array $options): array
|
||||||
|
{
|
||||||
|
if (empty($wordlist)) {
|
||||||
|
throw new RuntimeException('Wordlist is empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select random words with filtering
|
||||||
|
$wordEntries = $this->selectWords($options['words'], $wordlist, $options);
|
||||||
|
|
||||||
|
// Process words (apply space handling, length filtering, etc.)
|
||||||
|
$words = [];
|
||||||
|
$processedWordEntries = [];
|
||||||
|
foreach ($wordEntries as $entry) {
|
||||||
|
$processedEntry = $this->processWordEntry($entry, $options);
|
||||||
|
if ($processedEntry !== null) {
|
||||||
|
$words[] = $processedEntry['word'];
|
||||||
|
$processedWordEntries[] = $processedEntry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get more words if filtering removed too many
|
||||||
|
if (count($words) < $options['words']) {
|
||||||
|
$additionalNeeded = $options['words'] - count($words);
|
||||||
|
$additionalEntries = $this->selectWords($additionalNeeded * 3, $wordlist, $options);
|
||||||
|
|
||||||
|
foreach ($additionalEntries as $entry) {
|
||||||
|
if (count($words) >= $options['words']) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$processedEntry = $this->processWordEntry($entry, $options);
|
||||||
|
if ($processedEntry !== null) {
|
||||||
|
$words[] = $processedEntry['word'];
|
||||||
|
$processedWordEntries[] = $processedEntry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($words) < $options['words']) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
'Not enough suitable words found with current filtering options'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim to requested count
|
||||||
|
$words = array_slice($words, 0, $options['words']);
|
||||||
|
$processedWordEntries = array_slice($processedWordEntries, 0, $options['words']);
|
||||||
|
|
||||||
|
// Add number if requested
|
||||||
|
if ($options['add_number']) {
|
||||||
|
$words = $this->addNumberToWords($words, $options['number_position']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join with separators
|
||||||
|
$password = $this->joinWordsWithRandomSeparators($words, $options['separator']);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'password' => $password,
|
||||||
|
'method' => 'xkcdpass',
|
||||||
|
'word_count' => count($words),
|
||||||
|
'wordEntries' => $processedWordEntries,
|
||||||
|
'entropy' => $this->calculateEntropyForOptions($options)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process word entry with filtering options
|
||||||
|
*/
|
||||||
|
private function processWordEntry(array $entry, array $options): ?array
|
||||||
|
{
|
||||||
|
$word = $entry['word'];
|
||||||
|
$explanation = $entry['explanation'] ?? '';
|
||||||
|
|
||||||
|
// Apply word length filter
|
||||||
|
if ($options['max_word_length'] > 0 && strlen($word) > $options['max_word_length']) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply space handling
|
||||||
|
if (strpos($word, ' ') !== false) {
|
||||||
|
switch ($options['space_handling']) {
|
||||||
|
case 'skip':
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case 'modify':
|
||||||
|
$word = str_replace(' ', '_', $word);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'allow':
|
||||||
|
default:
|
||||||
|
// Keep as-is
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'word' => $word,
|
||||||
|
'explanation' => $explanation
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select random words from wordlist
|
||||||
|
*/
|
||||||
|
private function selectWords(int $count, array $wordlist, array $options): array
|
||||||
|
{
|
||||||
|
// Pre-filter based on options
|
||||||
|
$filtered = [];
|
||||||
|
foreach ($wordlist as $entry) {
|
||||||
|
$word = $entry['word'];
|
||||||
|
|
||||||
|
// Check length constraint
|
||||||
|
if ($options['max_word_length'] > 0 && strlen($word) > $options['max_word_length']) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check space handling
|
||||||
|
if ($options['space_handling'] === 'skip' && strpos($word, ' ') !== false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filtered[] = $entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to original if filtering removed everything
|
||||||
|
if (empty($filtered)) {
|
||||||
|
$filtered = $wordlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuffle and return
|
||||||
|
shuffle($filtered);
|
||||||
|
return array_slice($filtered, 0, min($count * 2, count($filtered)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Join words with random separator selection for each position
|
||||||
|
*/
|
||||||
|
private function joinWordsWithRandomSeparators(array $words, string $separatorSet): string
|
||||||
|
{
|
||||||
|
if (count($words) <= 1) {
|
||||||
|
return implode('', $words);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean separator set
|
||||||
|
$cleanSeparators = str_replace(' ', '', $separatorSet);
|
||||||
|
if (empty($cleanSeparators)) {
|
||||||
|
return implode('', $words);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $words[0];
|
||||||
|
for ($i = 1; $i < count($words); $i++) {
|
||||||
|
// Select random separator if multiple available
|
||||||
|
if (strlen($cleanSeparators) === 1) {
|
||||||
|
$separator = $cleanSeparators;
|
||||||
|
} else {
|
||||||
|
$chars = str_split($cleanSeparators);
|
||||||
|
$separator = $chars[array_rand($chars)];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result .= $separator . $words[$i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add random number to words
|
||||||
|
*/
|
||||||
|
private function addNumberToWords(array $words, string $position): array
|
||||||
|
{
|
||||||
|
$number = (string)random_int(10, 9999);
|
||||||
|
|
||||||
|
switch ($position) {
|
||||||
|
case 'beginning':
|
||||||
|
array_unshift($words, $number);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'random':
|
||||||
|
if (count($words) > 1) {
|
||||||
|
array_splice($words, random_int(0, count($words)), 0, [$number]);
|
||||||
|
} else {
|
||||||
|
$words[] = $number;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'end':
|
||||||
|
default:
|
||||||
|
$words[] = $number;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $words;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate XKCD entropy from actual option-constrained candidate space.
|
||||||
|
*/
|
||||||
|
public function calculateEntropyForOptions(array $options): float
|
||||||
|
{
|
||||||
|
$breakdown = $this->getEntropyBreakdownForOptions($options);
|
||||||
|
return (float) ($breakdown['entropy_bits'] ?? 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return detailed entropy contributors for transparent API/UI reporting.
|
||||||
|
*/
|
||||||
|
public function getEntropyBreakdownForOptions(array $options): array
|
||||||
|
{
|
||||||
|
$normalized = $this->normalizeEntropyOptions($options);
|
||||||
|
|
||||||
|
$requestedWords = max(1, (int) ($normalized['words'] ?? 4));
|
||||||
|
$wordlistCode = (string) ($normalized['wordlist'] ?? 'en');
|
||||||
|
$wordlistEntries = $this->getWordlist($wordlistCode);
|
||||||
|
$totalCandidates = count($wordlistEntries);
|
||||||
|
|
||||||
|
$effectiveCandidates = 0;
|
||||||
|
foreach ($wordlistEntries as $entry) {
|
||||||
|
$word = is_array($entry) ? (string) ($entry['word'] ?? '') : (string) $entry;
|
||||||
|
if ($word === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($normalized['max_word_length'] > 0 && strlen($word) > $normalized['max_word_length']) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($normalized['space_handling'] === 'skip' && strpos($word, ' ') !== false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$effectiveCandidates++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($effectiveCandidates <= 0) {
|
||||||
|
$effectiveCandidates = max(1, $totalCandidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
$wordEntropyBits = $requestedWords * log((float) $effectiveCandidates, 2);
|
||||||
|
|
||||||
|
$separatorSet = str_replace(' ', '', (string) ($normalized['separator'] ?? '-'));
|
||||||
|
$separatorAlphabet = count(array_unique(str_split($separatorSet)));
|
||||||
|
$separatorSlots = max(0, $requestedWords - 1);
|
||||||
|
$separatorEntropyBits = ($separatorAlphabet > 1 && $separatorSlots > 0)
|
||||||
|
? ($separatorSlots * log((float) $separatorAlphabet, 2))
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
|
$numberEntropyBits = 0.0;
|
||||||
|
if ((bool) ($normalized['add_number'] ?? false)) {
|
||||||
|
// random_int(10, 9999) includes 9,990 possible values.
|
||||||
|
$numberEntropyBits = log(9990.0, 2);
|
||||||
|
|
||||||
|
if (($normalized['number_position'] ?? 'end') === 'random') {
|
||||||
|
$numberEntropyBits += log((float) ($requestedWords + 1), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalEntropyBits = $wordEntropyBits + $separatorEntropyBits + $numberEntropyBits;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'entropy_model' => 'xkcd-candidate-space-v1',
|
||||||
|
'model_confidence' => 'high',
|
||||||
|
'wordlist' => $wordlistCode,
|
||||||
|
'requested_words' => $requestedWords,
|
||||||
|
'wordlist_total_candidates' => $totalCandidates,
|
||||||
|
'effective_wordlist_candidates' => $effectiveCandidates,
|
||||||
|
'max_word_length' => (int) $normalized['max_word_length'],
|
||||||
|
'space_handling' => (string) ($normalized['space_handling'] ?? 'allow'),
|
||||||
|
'separator_alphabet_size' => $separatorAlphabet,
|
||||||
|
'separator_slots' => $separatorSlots,
|
||||||
|
'number_inserted' => (bool) ($normalized['add_number'] ?? false),
|
||||||
|
'number_position' => (string) ($normalized['number_position'] ?? 'end'),
|
||||||
|
'word_entropy_bits' => round($wordEntropyBits, 1),
|
||||||
|
'separator_entropy_bits' => round($separatorEntropyBits, 1),
|
||||||
|
'number_entropy_bits' => round($numberEntropyBits, 1),
|
||||||
|
'entropy_bits' => round($totalEntropyBits, 1),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize option aliases used across API/frontend payloads.
|
||||||
|
*/
|
||||||
|
private function normalizeEntropyOptions(array $options): array
|
||||||
|
{
|
||||||
|
$normalized = $options;
|
||||||
|
|
||||||
|
if (!isset($normalized['wordlist']) && isset($normalized['language'])) {
|
||||||
|
$normalized['wordlist'] = $normalized['language'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($normalized['add_number']) && isset($normalized['addNumber'])) {
|
||||||
|
$normalized['add_number'] = $normalized['addNumber'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->validateOptions($normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize available wordlists
|
||||||
|
*/
|
||||||
|
private function initializeWordlists(): void
|
||||||
|
{
|
||||||
|
$defaultLabels = [
|
||||||
|
'en' => 'EFF Large Wordlist (English)',
|
||||||
|
'de' => 'German Common Words',
|
||||||
|
'fr' => 'French Common Words',
|
||||||
|
'it' => 'Italian Common Words',
|
||||||
|
'es' => 'Spanish Common Words'
|
||||||
|
];
|
||||||
|
|
||||||
|
$defaultFileMap = [
|
||||||
|
'en' => 'eff_large_wordlist_en.txt',
|
||||||
|
'de' => 'german_common_words_de.txt',
|
||||||
|
'fr' => 'french_common_words_fr.txt',
|
||||||
|
'it' => 'italian_common_words_it.txt',
|
||||||
|
'es' => 'spanish_common_words_es.txt'
|
||||||
|
];
|
||||||
|
|
||||||
|
$configuredWordlists = $this->getConfigValue('generator.xkcd.wordlists', null);
|
||||||
|
if (is_array($configuredWordlists) && !empty($configuredWordlists)) {
|
||||||
|
$labels = [];
|
||||||
|
$files = [];
|
||||||
|
|
||||||
|
foreach ($configuredWordlists as $code => $definition) {
|
||||||
|
if (!is_string($code) || trim($code) === '' || !is_array($definition)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$label = $definition['label'] ?? null;
|
||||||
|
$file = $definition['file'] ?? null;
|
||||||
|
|
||||||
|
if (!is_string($label) || trim($label) === '' || !is_string($file) || trim($file) === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedCode = trim($code);
|
||||||
|
$labels[$normalizedCode] = trim($label);
|
||||||
|
$files[$normalizedCode] = trim($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($labels) && !empty($files)) {
|
||||||
|
$this->availableWordlists = $labels;
|
||||||
|
$this->wordlistFileMap = $files;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($this->availableWordlists) || empty($this->wordlistFileMap)) {
|
||||||
|
$this->availableWordlists = $defaultLabels;
|
||||||
|
$this->wordlistFileMap = $defaultFileMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->logger && $this->isLogLevel('debug')) {
|
||||||
|
$this->logger->debug('XkcdPass: wordlists initialized', [
|
||||||
|
'count' => count($this->availableWordlists),
|
||||||
|
'configured' => !empty($configuredWordlists ?? null)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load wordlist from file
|
||||||
|
*/
|
||||||
|
private function getWordlist(string $code): array
|
||||||
|
{
|
||||||
|
if (isset($this->wordlists[$code])) {
|
||||||
|
return $this->wordlists[$code];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load wordlist
|
||||||
|
$this->loadWordlist($code);
|
||||||
|
|
||||||
|
return $this->wordlists[$code] ?? $this->getFallbackWordlist();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load wordlist from file
|
||||||
|
*/
|
||||||
|
private function loadWordlist(string $code): void
|
||||||
|
{
|
||||||
|
$filename = $this->getWordlistFilename($code);
|
||||||
|
$path = $this->getWordlistPath() . '/' . $filename;
|
||||||
|
|
||||||
|
if (!file_exists($path) || !is_readable($path)) {
|
||||||
|
if ($this->logger) {
|
||||||
|
$this->logger->warning('XkcdPass: wordlist file not found', [
|
||||||
|
'code' => $code,
|
||||||
|
'path' => $path
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$this->wordlists[$code] = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($path);
|
||||||
|
if ($content === false) {
|
||||||
|
if ($this->logger) {
|
||||||
|
$this->logger->warning('XkcdPass: failed to read wordlist file', [
|
||||||
|
'path' => $path
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$this->wordlists[$code] = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = array_filter(
|
||||||
|
array_map('trim', explode("\n", $content)),
|
||||||
|
fn($line) => !empty($line) && strlen($line) >= 3
|
||||||
|
);
|
||||||
|
|
||||||
|
$processed = [];
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$parsed = $this->parseWordlistEntry($line);
|
||||||
|
if (!empty($parsed['word'])) {
|
||||||
|
$processed[] = $parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->wordlists[$code] = !empty($processed) ? $processed : [];
|
||||||
|
|
||||||
|
if ($this->logger && $this->isLogLevel('debug')) {
|
||||||
|
$this->logger->debug('XkcdPass: wordlist loaded', [
|
||||||
|
'code' => $code,
|
||||||
|
'count' => count($this->wordlists[$code])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get fallback wordlist
|
||||||
|
*/
|
||||||
|
private function getFallbackWordlist(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['word' => 'correct', 'explanation' => ''],
|
||||||
|
['word' => 'horse', 'explanation' => ''],
|
||||||
|
['word' => 'battery', 'explanation' => ''],
|
||||||
|
['word' => 'staple', 'explanation' => ''],
|
||||||
|
['word' => 'python', 'explanation' => ''],
|
||||||
|
['word' => 'monkey', 'explanation' => ''],
|
||||||
|
['word' => 'pencil', 'explanation' => ''],
|
||||||
|
['word' => 'cloud', 'explanation' => ''],
|
||||||
|
['word' => 'mountain', 'explanation' => ''],
|
||||||
|
['word' => 'rainbow', 'explanation' => '']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get wordlist filename
|
||||||
|
*/
|
||||||
|
private function getWordlistFilename(string $code): string
|
||||||
|
{
|
||||||
|
return $this->wordlistFileMap[$code] ?? ($this->wordlistFileMap['en'] ?? 'eff_large_wordlist_en.txt');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get wordlist directory
|
||||||
|
*/
|
||||||
|
private function getWordlistPath(): string
|
||||||
|
{
|
||||||
|
$configuredPath = $this->getConfigValue('generator.xkcd.wordlists_path', self::DEFAULT_WORDLISTS_DIR);
|
||||||
|
$safePath = is_string($configuredPath) && trim($configuredPath) !== ''
|
||||||
|
? trim($configuredPath)
|
||||||
|
: self::DEFAULT_WORDLISTS_DIR;
|
||||||
|
|
||||||
|
if (str_starts_with($safePath, '/')) {
|
||||||
|
return $safePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dirname(__DIR__, 2) . '/' . ltrim($safePath, '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,586 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Managers;
|
||||||
|
|
||||||
|
use App\Generators\HighEntropyGenerator;
|
||||||
|
use App\Services\LLM\LLMManager;
|
||||||
|
use App\Utils\ConfigLoader;
|
||||||
|
|
||||||
|
class PasswordGeneratorManager
|
||||||
|
{
|
||||||
|
private array $generators = [];
|
||||||
|
private ?LLMManager $llmManager = null;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
// Load config
|
||||||
|
$config = $this->loadConfig();
|
||||||
|
|
||||||
|
// Initialize LLM Manager
|
||||||
|
if (isset($config['llm'])) {
|
||||||
|
$this->llmManager = new LLMManager($config['llm']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register generators
|
||||||
|
$this->registerGenerators();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadConfig(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$config = ConfigLoader::load();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$config = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$provider = $_ENV['LLM_DEFAULT_PROVIDER'] ?? null;
|
||||||
|
if (is_string($provider) && $provider !== '') {
|
||||||
|
$config['llm']['default_provider'] = $provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKeyEnvMap = [
|
||||||
|
'openai' => 'OPENAI_API_KEY',
|
||||||
|
'claude' => 'CLAUDE_API_KEY',
|
||||||
|
'gemini' => 'GEMINI_API_KEY',
|
||||||
|
'perplexity' => 'PERPLEXITY_API_KEY',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($apiKeyEnvMap as $providerName => $envVar) {
|
||||||
|
$apiKey = $_ENV[$envVar] ?? null;
|
||||||
|
if (is_string($apiKey) && $apiKey !== '') {
|
||||||
|
$config['llm']['api_keys'][$providerName] = $apiKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_array($config) ? $config : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function registerGenerators(): void
|
||||||
|
{
|
||||||
|
$this->generators = [
|
||||||
|
'high-entropy' => new \App\Generators\HighEntropyGenerator(),
|
||||||
|
'xkcdpass' => new \App\Generators\XkcdPassGenerator($this->llmManager),
|
||||||
|
'diceware' => new \App\Generators\DicewareGenerator(),
|
||||||
|
'pwgen' => new \App\Generators\PwgenGenerator(),
|
||||||
|
'passkey' => new \App\Generators\PasskeyGenerator(),
|
||||||
|
'phrase-abbreviator' => new \App\Generators\PhraseAbbreviatorGenerator($this->llmManager),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAvailableTypes(): array
|
||||||
|
{
|
||||||
|
$types = [];
|
||||||
|
foreach ($this->generators as $key => $generator) {
|
||||||
|
$types[] = [
|
||||||
|
'id' => $key,
|
||||||
|
'name' => $generator->getName(),
|
||||||
|
'description' => $generator->getDescription(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $types;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGeneratorDefinitions(): array
|
||||||
|
{
|
||||||
|
$definitions = [];
|
||||||
|
|
||||||
|
foreach ($this->generators as $id => $generator) {
|
||||||
|
$optionDefinitions = [];
|
||||||
|
foreach ($generator->getOptionDefinitions() as $optionName => $definition) {
|
||||||
|
$group = $definition['group'] ?? 'basic';
|
||||||
|
$label = $definition['label'] ?? $this->humanizeOptionName($optionName);
|
||||||
|
$description = $definition['description'] ?? '';
|
||||||
|
|
||||||
|
$option = [
|
||||||
|
'name' => $optionName,
|
||||||
|
'type' => $definition['type'],
|
||||||
|
'label' => $label,
|
||||||
|
'description' => $description,
|
||||||
|
'labelKey' => $id . '_option_' . $optionName,
|
||||||
|
'descriptionKey' => $id . '_option_' . $optionName . '_description',
|
||||||
|
'default' => $definition['default'] ?? null,
|
||||||
|
'required' => $definition['required'] ?? false,
|
||||||
|
'group' => $group,
|
||||||
|
'groupTitleKey' => $id . '_options_group_' . $group . '_title',
|
||||||
|
'validationErrorKey' => $id . '_option_' . $optionName . '_validation_error',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isset($definition['min'])) {
|
||||||
|
$option['min'] = $definition['min'];
|
||||||
|
}
|
||||||
|
if (isset($definition['max'])) {
|
||||||
|
$option['max'] = $definition['max'];
|
||||||
|
}
|
||||||
|
if (isset($definition['minLength'])) {
|
||||||
|
$option['minLength'] = $definition['minLength'];
|
||||||
|
}
|
||||||
|
if (isset($definition['maxLength'])) {
|
||||||
|
$option['maxLength'] = $definition['maxLength'];
|
||||||
|
}
|
||||||
|
if (isset($definition['pattern'])) {
|
||||||
|
$option['pattern'] = $definition['pattern'];
|
||||||
|
}
|
||||||
|
if (isset($definition['translateChoices'])) {
|
||||||
|
$option['translateChoices'] = $definition['translateChoices'];
|
||||||
|
}
|
||||||
|
if (isset($definition['choices'])) {
|
||||||
|
$formattedChoices = [];
|
||||||
|
foreach ($definition['choices'] as $choice) {
|
||||||
|
if (is_array($choice)) {
|
||||||
|
$value = $choice['value'];
|
||||||
|
$label = $choice['label'];
|
||||||
|
} else {
|
||||||
|
$value = $choice;
|
||||||
|
$label = $choice;
|
||||||
|
}
|
||||||
|
|
||||||
|
$formattedChoices[] = [
|
||||||
|
'value' => $value,
|
||||||
|
'label' => $label,
|
||||||
|
'labelKey' => $id . '_option_' . $optionName . '_choice_' . $value,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$option['choices'] = $formattedChoices;
|
||||||
|
}
|
||||||
|
|
||||||
|
$optionDefinitions[] = $option;
|
||||||
|
}
|
||||||
|
|
||||||
|
$definitions[$id] = [
|
||||||
|
'id' => $id,
|
||||||
|
'name' => $generator->getName(),
|
||||||
|
'description' => $generator->getDescription(),
|
||||||
|
'nameKey' => $id . '_title',
|
||||||
|
'descriptionKey' => $id . '_description',
|
||||||
|
'version' => $generator->getVersion(),
|
||||||
|
'options' => $optionDefinitions,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $definitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function humanizeOptionName(string $optionName): string
|
||||||
|
{
|
||||||
|
$humanized = preg_replace('/[_-]+/', ' ', $optionName);
|
||||||
|
$humanized = preg_replace('/(?<!^)([A-Z])/', ' $1', (string) $humanized);
|
||||||
|
return trim(ucwords((string) $humanized));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGenerator(string $type)
|
||||||
|
{
|
||||||
|
if (!isset($this->generators[$type])) {
|
||||||
|
throw new \InvalidArgumentException("Unknown generator type: $type");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->generators[$type];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhanced generate method with proper data extraction
|
||||||
|
*/
|
||||||
|
public function generate(string $type, int $count, array $options): array
|
||||||
|
{
|
||||||
|
$generator = $this->getGenerator($type);
|
||||||
|
$results = $generator->generate($count, $options);
|
||||||
|
|
||||||
|
// Initialize response data
|
||||||
|
$passwords = [];
|
||||||
|
$explanations = [];
|
||||||
|
$diceRolls = [];
|
||||||
|
$securityInfo = null;
|
||||||
|
|
||||||
|
// Process each result and extract all nested data
|
||||||
|
for ($i = 0; $i < count($results); $i++) {
|
||||||
|
$result = $results[$i];
|
||||||
|
|
||||||
|
if (is_array($result) && isset($result['password'])) {
|
||||||
|
// Extract password
|
||||||
|
$passwords[] = $result['password'];
|
||||||
|
|
||||||
|
// Extract explanations from wordEntries
|
||||||
|
$explanation = null;
|
||||||
|
if (!empty($result['wordEntries'])) {
|
||||||
|
$explanationTexts = [];
|
||||||
|
foreach ($result['wordEntries'] as $entry) {
|
||||||
|
if (is_array($entry) && isset($entry['explanation']) && !empty($entry['explanation'])) {
|
||||||
|
$explanationTexts[] = $entry['explanation'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($explanationTexts)) {
|
||||||
|
$explanation = implode(' – ', $explanationTexts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$explanations[] = $explanation;
|
||||||
|
|
||||||
|
// Extract dice rolls (for Diceware)
|
||||||
|
if (!empty($result['dice_rolls'])) {
|
||||||
|
$diceRolls[] = $result['dice_rolls'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract security info (for Diceware)
|
||||||
|
if (!empty($result['security_info'])) {
|
||||||
|
$securityInfo = $result['security_info'];
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Simple string-only format
|
||||||
|
$passwords[] = is_string($result) ? $result : (string)$result;
|
||||||
|
$explanations[] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate accurate entropy and optional model breakdown
|
||||||
|
$entropyAnalysis = $this->calculateEntropyAnalysis($generator, $options, $passwords[0] ?? '');
|
||||||
|
$entropyBits = $entropyAnalysis['entropy_bits'];
|
||||||
|
|
||||||
|
// Build response
|
||||||
|
$response = [
|
||||||
|
'passwords' => $passwords,
|
||||||
|
'entropy_bits' => $entropyBits,
|
||||||
|
'generator_info' => $generator->getName() . ' generator',
|
||||||
|
'options_used' => $options
|
||||||
|
];
|
||||||
|
|
||||||
|
// FIXED: Only add explanations if any exist and are not null
|
||||||
|
$validExplanations = array_filter($explanations, fn($exp) => !empty($exp));
|
||||||
|
if (!empty($validExplanations)) {
|
||||||
|
// Preserve array indices to match passwords
|
||||||
|
$response['explanations'] = $explanations;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXED: Only add dice rolls if any exist
|
||||||
|
if (!empty($diceRolls)) {
|
||||||
|
$response['dice_rolls'] = $diceRolls;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXED: Add security info if exists
|
||||||
|
if ($securityInfo !== null) {
|
||||||
|
$response['security_info'] = $securityInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
$assessment = $this->buildSecurityAssessment(
|
||||||
|
$entropyBits,
|
||||||
|
$generator->getName(),
|
||||||
|
$securityInfo,
|
||||||
|
$passwords[0] ?? '',
|
||||||
|
$entropyAnalysis['details'] ?? []
|
||||||
|
);
|
||||||
|
|
||||||
|
$response['security_assessment'] = $assessment['security_assessment'];
|
||||||
|
$response['raw_technical_details'] = $assessment['raw_technical_details'];
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildSecurityAssessment(float $entropyBits, string $generatorName, ?array $securityInfo, string $password, array $entropyDetails = []): array
|
||||||
|
{
|
||||||
|
$rawDetails = [
|
||||||
|
'entropy_bits' => round($entropyBits, 1),
|
||||||
|
'generator' => $generatorName,
|
||||||
|
'length' => $password !== '' ? strlen($password) : 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($entropyDetails)) {
|
||||||
|
$rawDetails['entropy_breakdown'] = $entropyDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($securityInfo)) {
|
||||||
|
$rawDetails = array_merge($rawDetails, $securityInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
$securityLevel = $this->normalizeSecurityLevel($rawDetails['security_level'] ?? '', $entropyBits);
|
||||||
|
$rawDetails['security_level'] = $securityLevel;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'security_assessment' => $this->getSecurityAssessmentText($securityLevel, $entropyBits),
|
||||||
|
'raw_technical_details' => $rawDetails,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeSecurityLevel(string $securityLevel, float $entropyBits): string
|
||||||
|
{
|
||||||
|
$normalized = strtolower(trim($securityLevel));
|
||||||
|
|
||||||
|
if ($normalized === '' || $normalized === 'none') {
|
||||||
|
return $this->classifyEntropy($entropyBits);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($normalized, 'excellent') || str_contains($normalized, 'very strong')) {
|
||||||
|
return 'Very strong';
|
||||||
|
}
|
||||||
|
if (str_contains($normalized, 'strong')) {
|
||||||
|
return 'Strong';
|
||||||
|
}
|
||||||
|
if (str_contains($normalized, 'adequate')) {
|
||||||
|
return 'Adequate';
|
||||||
|
}
|
||||||
|
if (str_contains($normalized, 'weak')) {
|
||||||
|
return 'Weak';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->classifyEntropy($entropyBits);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function classifyEntropy(float $entropyBits): string
|
||||||
|
{
|
||||||
|
if ($entropyBits >= 128) {
|
||||||
|
return 'Very strong';
|
||||||
|
}
|
||||||
|
if ($entropyBits >= 96) {
|
||||||
|
return 'Strong';
|
||||||
|
}
|
||||||
|
if ($entropyBits >= 64) {
|
||||||
|
return 'Adequate';
|
||||||
|
}
|
||||||
|
if ($entropyBits >= 40) {
|
||||||
|
return 'Weak';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Very weak';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSecurityAssessmentText(string $securityLevel, float $entropyBits): string
|
||||||
|
{
|
||||||
|
$level = trim($securityLevel) !== '' ? $securityLevel : $this->classifyEntropy($entropyBits);
|
||||||
|
|
||||||
|
$messages = [
|
||||||
|
'Very strong' => 'Very strong — this password has excellent entropy and is suitable for high-value accounts.',
|
||||||
|
'Strong' => 'Strong — this password has good entropy and is suitable for most uses.',
|
||||||
|
'Adequate' => 'Adequate — this password is acceptable for ordinary use but can be improved.',
|
||||||
|
'Weak' => 'Weak — this password is relatively easy to brute-force and should be replaced.',
|
||||||
|
'Very weak' => 'Very weak — this password offers very little protection.',
|
||||||
|
];
|
||||||
|
|
||||||
|
return $messages[$level] ?? 'Security assessment unavailable.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateOptions(string $type, array $options): array
|
||||||
|
{
|
||||||
|
$generator = $this->getGenerator($type);
|
||||||
|
return $generator->validateOptions($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDefaultOptions(string $type): array
|
||||||
|
{
|
||||||
|
$generator = $this->getGenerator($type);
|
||||||
|
return $generator->getDefaultOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accurate entropy calculation using generator-specific logic
|
||||||
|
*/
|
||||||
|
private function calculateAccurateEntropy($generator, array $options, string $samplePassword): float
|
||||||
|
{
|
||||||
|
// Try generator-specific calculation first
|
||||||
|
if (method_exists($generator, 'calculateEntropyForOptions')) {
|
||||||
|
return $generator->calculateEntropyForOptions($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to type-specific calculations
|
||||||
|
$generatorName = strtolower($generator->getName());
|
||||||
|
|
||||||
|
switch ($generatorName) {
|
||||||
|
case 'xkcd pass':
|
||||||
|
case 'xkcdpass':
|
||||||
|
return $this->calculateXkcdEntropy($options);
|
||||||
|
|
||||||
|
case 'diceware':
|
||||||
|
return $this->calculateDicewareEntropy($options);
|
||||||
|
|
||||||
|
case 'passkey':
|
||||||
|
return $this->calculatePasskeyEntropy($options);
|
||||||
|
|
||||||
|
case 'high entropy':
|
||||||
|
return $this->calculateHighEntropyEntropy($options);
|
||||||
|
|
||||||
|
case 'pwgen':
|
||||||
|
return $this->calculatePwgenEntropy($options, $samplePassword);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return $this->calculateGenericEntropy($samplePassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate entropy and expose optional generator-level entropy details.
|
||||||
|
*/
|
||||||
|
private function calculateEntropyAnalysis($generator, array $options, string $samplePassword): array
|
||||||
|
{
|
||||||
|
$details = [];
|
||||||
|
|
||||||
|
if (method_exists($generator, 'getEntropyBreakdownForOptions')) {
|
||||||
|
try {
|
||||||
|
$details = $generator->getEntropyBreakdownForOptions($options);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$details = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$entropyBits = $this->calculateAccurateEntropy($generator, $options, $samplePassword);
|
||||||
|
if (isset($details['entropy_bits']) && is_numeric($details['entropy_bits'])) {
|
||||||
|
$entropyBits = (float) $details['entropy_bits'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'entropy_bits' => round($entropyBits, 1),
|
||||||
|
'details' => $details,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate XKCD passphrase entropy with filtering effects
|
||||||
|
*/
|
||||||
|
private function calculateXkcdEntropy(array $options): float
|
||||||
|
{
|
||||||
|
$words = $options['words'] ?? 4;
|
||||||
|
$addNumber = $options['addNumber'] ?? false;
|
||||||
|
$language = $options['language'] ?? 'en';
|
||||||
|
|
||||||
|
// Base wordlist sizes by language
|
||||||
|
$wordlistSizes = [
|
||||||
|
'en' => 7776, // EFF Large wordlist
|
||||||
|
'de' => 5000, // Estimated German words
|
||||||
|
'fr' => 5000, // Estimated French words
|
||||||
|
'it' => 4000, // Estimated Italian words
|
||||||
|
'es' => 4000, // Estimated Spanish words
|
||||||
|
'ch' => 3000, // Estimated Swiss German words
|
||||||
|
];
|
||||||
|
|
||||||
|
$baseWordlistSize = $wordlistSizes[$language] ?? 7776;
|
||||||
|
|
||||||
|
// Apply filtering penalties
|
||||||
|
$effectiveWordlistSize = $baseWordlistSize;
|
||||||
|
|
||||||
|
// Word length filter penalty
|
||||||
|
if (isset($options['max_word_length']) && $options['max_word_length'] > 0) {
|
||||||
|
$maxLength = $options['max_word_length'];
|
||||||
|
if ($maxLength <= 4) {
|
||||||
|
$effectiveWordlistSize *= 0.3; // Very restrictive
|
||||||
|
} elseif ($maxLength <= 6) {
|
||||||
|
$effectiveWordlistSize *= 0.6; // Moderately restrictive
|
||||||
|
} elseif ($maxLength <= 8) {
|
||||||
|
$effectiveWordlistSize *= 0.85; // Slightly restrictive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Space handling penalty
|
||||||
|
if (isset($options['space_handling']) && $options['space_handling'] === 'skip') {
|
||||||
|
$effectiveWordlistSize *= 0.95; // ~5% of words contain spaces
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure reasonable minimum
|
||||||
|
$effectiveWordlistSize = max($effectiveWordlistSize, 50);
|
||||||
|
|
||||||
|
// Calculate word entropy
|
||||||
|
$wordEntropy = $words * log($effectiveWordlistSize, 2);
|
||||||
|
|
||||||
|
// Add number entropy if used
|
||||||
|
$numberEntropy = 0;
|
||||||
|
if ($addNumber) {
|
||||||
|
$numberEntropy = log(90, 2); // 2-digit numbers: 10-99 = 90 possibilities
|
||||||
|
}
|
||||||
|
|
||||||
|
return round($wordEntropy + $numberEntropy, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate Diceware entropy
|
||||||
|
*/
|
||||||
|
private function calculateDicewareEntropy(array $options): float
|
||||||
|
{
|
||||||
|
$words = $options['words'] ?? 6;
|
||||||
|
|
||||||
|
// Diceware standard: 7776 words (6^5)
|
||||||
|
$entropy = $words * log(7776, 2);
|
||||||
|
|
||||||
|
return round($entropy, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate Passkey entropy
|
||||||
|
*/
|
||||||
|
private function calculatePasskeyEntropy(array $options): float
|
||||||
|
{
|
||||||
|
$syllables = $options['syllables'] ?? 6;
|
||||||
|
|
||||||
|
// Estimate combinations per syllable
|
||||||
|
// CV: 21×5=105, CVC: 21×5×21=2205, average ≈ 150
|
||||||
|
$combinationsPerSyllable = 150;
|
||||||
|
|
||||||
|
// Character substitutions add ~20% entropy
|
||||||
|
$substitutionMultiplier = 1.2;
|
||||||
|
|
||||||
|
$totalCombinations = pow($combinationsPerSyllable, $syllables) * $substitutionMultiplier;
|
||||||
|
|
||||||
|
return round(log($totalCombinations, 2), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate High Entropy password entropy
|
||||||
|
*/
|
||||||
|
private function calculateHighEntropyEntropy(array $options): float
|
||||||
|
{
|
||||||
|
$length = $options['length'] ?? 16;
|
||||||
|
|
||||||
|
// Count character set size
|
||||||
|
$charsetSize = 0;
|
||||||
|
if ($options['include_lowercase'] ?? true) $charsetSize += 26;
|
||||||
|
if ($options['include_uppercase'] ?? true) $charsetSize += 26;
|
||||||
|
if ($options['include_numbers'] ?? true) $charsetSize += 10;
|
||||||
|
if ($options['include_symbols'] ?? true) $charsetSize += 32;
|
||||||
|
|
||||||
|
// Ensure at least one character type
|
||||||
|
$charsetSize = max($charsetSize, 26);
|
||||||
|
|
||||||
|
return round($length * log($charsetSize, 2), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate PWGen entropy estimate
|
||||||
|
*/
|
||||||
|
private function calculatePwgenEntropy(array $options, string $samplePassword): float
|
||||||
|
{
|
||||||
|
$type = $options['type'] ?? '16-char';
|
||||||
|
|
||||||
|
// PWGen entropy estimates based on pattern complexity
|
||||||
|
$entropyMap = [
|
||||||
|
'8-char' => 35.0,
|
||||||
|
'12-char' => 45.0,
|
||||||
|
'16-char' => 55.0
|
||||||
|
];
|
||||||
|
|
||||||
|
return $entropyMap[$type] ?? $this->calculateGenericEntropy($samplePassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic entropy calculation for unknown generators
|
||||||
|
*/
|
||||||
|
private function calculateGenericEntropy(string $password): float
|
||||||
|
{
|
||||||
|
if (empty($password)) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$length = strlen($password);
|
||||||
|
|
||||||
|
// Analyze character set used
|
||||||
|
$hasLower = preg_match('/[a-z]/', $password);
|
||||||
|
$hasUpper = preg_match('/[A-Z]/', $password);
|
||||||
|
$hasDigit = preg_match('/[0-9]/', $password);
|
||||||
|
$hasSymbol = preg_match('/[^a-zA-Z0-9\s]/', $password);
|
||||||
|
|
||||||
|
$charsetSize = 0;
|
||||||
|
if ($hasLower) $charsetSize += 26;
|
||||||
|
if ($hasUpper) $charsetSize += 26;
|
||||||
|
if ($hasDigit) $charsetSize += 10;
|
||||||
|
if ($hasSymbol) $charsetSize += 32; // Estimate
|
||||||
|
|
||||||
|
$charsetSize = max($charsetSize, 26); // Minimum alphabet
|
||||||
|
|
||||||
|
return round($length * log($charsetSize, 2), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Middleware;
|
||||||
|
|
||||||
|
use App\Utils\SecurityConfig;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface as Middleware;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||||
|
|
||||||
|
class CorsMiddleware implements Middleware
|
||||||
|
{
|
||||||
|
public function process(Request $request, RequestHandler $handler): Response
|
||||||
|
{
|
||||||
|
$response = $handler->handle($request);
|
||||||
|
$origin = SecurityConfig::resolveCorsOrigin($_ENV['APP_ENV'] ?? 'production');
|
||||||
|
|
||||||
|
if ($origin === '') {
|
||||||
|
$response = $response->withoutHeader('Access-Control-Allow-Origin');
|
||||||
|
} else {
|
||||||
|
$response = $response->withHeader('Access-Control-Allow-Origin', $origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response
|
||||||
|
->withHeader('Access-Control-Allow-Headers', $_ENV['CORS_HEADERS'] ?? 'Content-Type,Authorization')
|
||||||
|
->withHeader('Access-Control-Allow-Methods', $_ENV['CORS_METHODS'] ?? 'GET,POST,OPTIONS')
|
||||||
|
->withHeader('Access-Control-Max-Age', '3600');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Middleware;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface as Middleware;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||||
|
|
||||||
|
class JsonResponseMiddleware implements Middleware
|
||||||
|
{
|
||||||
|
public function process(Request $request, RequestHandler $handler): Response
|
||||||
|
{
|
||||||
|
$response = $handler->handle($request);
|
||||||
|
|
||||||
|
// Add JSON content type for API routes
|
||||||
|
if (strpos($request->getUri()->getPath(), '/api/') === 0) {
|
||||||
|
if (!$response->hasHeader('Content-Type')) {
|
||||||
|
$response = $response->withHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Middleware;
|
||||||
|
|
||||||
|
use App\Utils\SecurityConfig;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface as Middleware;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||||
|
|
||||||
|
class RateLimitMiddleware implements Middleware
|
||||||
|
{
|
||||||
|
private const CACHE_PREFIX = 'rate_limit_';
|
||||||
|
private int $maxRequests;
|
||||||
|
private int $timeWindow = 60; // seconds
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->maxRequests = SecurityConfig::resolveRateLimit($_ENV['APP_ENV'] ?? 'production', 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process(Request $request, RequestHandler $handler): Response
|
||||||
|
{
|
||||||
|
$clientIp = $this->getClientIp($request);
|
||||||
|
$cacheKey = self::CACHE_PREFIX . $clientIp;
|
||||||
|
|
||||||
|
// Simple in-memory rate limiting for demo purposes
|
||||||
|
// In production, use Redis or database
|
||||||
|
static $requests = [];
|
||||||
|
|
||||||
|
$now = time();
|
||||||
|
$windowStart = $now - $this->timeWindow;
|
||||||
|
|
||||||
|
// Clean old requests
|
||||||
|
if (isset($requests[$cacheKey])) {
|
||||||
|
$requests[$cacheKey] = array_filter(
|
||||||
|
$requests[$cacheKey],
|
||||||
|
fn($timestamp) => $timestamp > $windowStart
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$requests[$cacheKey] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if limit exceeded
|
||||||
|
if (count($requests[$cacheKey]) >= $this->maxRequests) {
|
||||||
|
$response = new \Slim\Psr7\Response();
|
||||||
|
$response->getBody()->write(json_encode([
|
||||||
|
'error' => 'Rate limit exceeded',
|
||||||
|
'message' => "Maximum {$this->maxRequests} requests per minute allowed"
|
||||||
|
]));
|
||||||
|
return $response
|
||||||
|
->withStatus(429)
|
||||||
|
->withHeader('Content-Type', 'application/json')
|
||||||
|
->withHeader('Retry-After', $this->timeWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add current request
|
||||||
|
$requests[$cacheKey][] = $now;
|
||||||
|
|
||||||
|
$response = $handler->handle($request);
|
||||||
|
|
||||||
|
// Add rate limit headers
|
||||||
|
$remaining = max(0, $this->maxRequests - count($requests[$cacheKey]));
|
||||||
|
return $response
|
||||||
|
->withHeader('X-RateLimit-Limit', (string) $this->maxRequests)
|
||||||
|
->withHeader('X-RateLimit-Remaining', (string) $remaining)
|
||||||
|
->withHeader('X-RateLimit-Reset', (string) ($now + $this->timeWindow));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getClientIp(Request $request): string
|
||||||
|
{
|
||||||
|
$serverParams = $request->getServerParams();
|
||||||
|
|
||||||
|
if (!empty($serverParams['HTTP_CLIENT_IP'])) {
|
||||||
|
return $serverParams['HTTP_CLIENT_IP'];
|
||||||
|
} elseif (!empty($serverParams['HTTP_X_FORWARDED_FOR'])) {
|
||||||
|
return $serverParams['HTTP_X_FORWARDED_FOR'];
|
||||||
|
} elseif (!empty($serverParams['REMOTE_ADDR'])) {
|
||||||
|
return $serverParams['REMOTE_ADDR'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '127.0.0.1';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\LLM;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract LLM Provider Base Class
|
||||||
|
*
|
||||||
|
* Provides common functionality for all LLM providers
|
||||||
|
*/
|
||||||
|
abstract class AbstractLLMProvider implements LLMProviderInterface
|
||||||
|
{
|
||||||
|
protected string $apiKey;
|
||||||
|
protected int $timeout;
|
||||||
|
protected int $maxTokens;
|
||||||
|
protected float $temperature;
|
||||||
|
|
||||||
|
public function __construct(array $config = [])
|
||||||
|
{
|
||||||
|
$this->apiKey = $config['api_key'] ?? '';
|
||||||
|
$this->timeout = $config['timeout'] ?? 30;
|
||||||
|
$this->maxTokens = $config['max_tokens'] ?? 500;
|
||||||
|
$this->temperature = $config['temperature'] ?? 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if provider is available (has API key)
|
||||||
|
*/
|
||||||
|
public function isAvailable(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make HTTP request to API
|
||||||
|
*/
|
||||||
|
protected function makeRequest(
|
||||||
|
string $url,
|
||||||
|
array $headers,
|
||||||
|
array $data,
|
||||||
|
string $method = 'POST'
|
||||||
|
): array {
|
||||||
|
$options = [
|
||||||
|
'http' => [
|
||||||
|
'method' => $method,
|
||||||
|
'header' => $this->buildHeaders($headers),
|
||||||
|
'content' => json_encode($data),
|
||||||
|
'timeout' => $this->timeout,
|
||||||
|
'ignore_errors' => true
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$context = stream_context_create($options);
|
||||||
|
$response = @file_get_contents($url, false, $context);
|
||||||
|
|
||||||
|
if ($response === false) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Failed to connect to {$this->getName()} API"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get HTTP response code
|
||||||
|
$httpCode = 200;
|
||||||
|
if (isset($http_response_header[0])) {
|
||||||
|
preg_match('/\d{3}/', $http_response_header[0], $matches);
|
||||||
|
$httpCode = (int)($matches[0] ?? 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = json_decode($response, true);
|
||||||
|
|
||||||
|
if ($httpCode >= 400) {
|
||||||
|
$errorMsg = $this->extractErrorMessage($result);
|
||||||
|
throw new RuntimeException(
|
||||||
|
"{$this->getName()} API error (HTTP {$httpCode}): {$errorMsg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build HTTP headers array into string
|
||||||
|
*/
|
||||||
|
private function buildHeaders(array $headers): string
|
||||||
|
{
|
||||||
|
$headerLines = [];
|
||||||
|
foreach ($headers as $key => $value) {
|
||||||
|
$headerLines[] = "{$key}: {$value}";
|
||||||
|
}
|
||||||
|
return implode("\r\n", $headerLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract error message from API response
|
||||||
|
*/
|
||||||
|
protected function extractErrorMessage(array $response): string
|
||||||
|
{
|
||||||
|
// Try common error message fields
|
||||||
|
return $response['error']['message']
|
||||||
|
?? $response['error']
|
||||||
|
?? $response['message']
|
||||||
|
?? 'Unknown error';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\LLM;
|
||||||
|
|
||||||
|
use App\Services\LLM\Providers\OpenAIProvider;
|
||||||
|
use App\Services\LLM\Providers\ClaudeProvider;
|
||||||
|
use App\Services\LLM\Providers\GeminiProvider;
|
||||||
|
use App\Services\LLM\Providers\PerplexityProvider;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM Manager
|
||||||
|
*
|
||||||
|
* Central service for managing multiple LLM providers
|
||||||
|
*/
|
||||||
|
class LLMManager
|
||||||
|
{
|
||||||
|
private array $providers = [];
|
||||||
|
private string $defaultProvider;
|
||||||
|
private array $config;
|
||||||
|
|
||||||
|
public function __construct(array $config = [])
|
||||||
|
{
|
||||||
|
$this->config = $config;
|
||||||
|
$this->defaultProvider = $config['default_provider'] ?? 'openai';
|
||||||
|
$this->initializeProviders();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize all LLM providers
|
||||||
|
*/
|
||||||
|
private function initializeProviders(): void
|
||||||
|
{
|
||||||
|
$apiKeys = $this->config['api_keys'] ?? [];
|
||||||
|
$llmConfig = $this->config;
|
||||||
|
|
||||||
|
// OpenAI
|
||||||
|
if (!empty($apiKeys['openai'])) {
|
||||||
|
$this->providers['openai'] = new OpenAIProvider([
|
||||||
|
'api_key' => $apiKeys['openai'],
|
||||||
|
'timeout' => $llmConfig['timeout'] ?? 30,
|
||||||
|
'max_tokens' => $llmConfig['max_tokens'] ?? 500,
|
||||||
|
'temperature' => $llmConfig['temperature'] ?? 0.7
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claude
|
||||||
|
if (!empty($apiKeys['claude'])) {
|
||||||
|
$this->providers['claude'] = new ClaudeProvider([
|
||||||
|
'api_key' => $apiKeys['claude'],
|
||||||
|
'timeout' => $llmConfig['timeout'] ?? 30,
|
||||||
|
'max_tokens' => $llmConfig['max_tokens'] ?? 500,
|
||||||
|
'temperature' => $llmConfig['temperature'] ?? 0.7
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gemini
|
||||||
|
if (!empty($apiKeys['gemini'])) {
|
||||||
|
$this->providers['gemini'] = new GeminiProvider([
|
||||||
|
'api_key' => $apiKeys['gemini'],
|
||||||
|
'timeout' => $llmConfig['timeout'] ?? 30,
|
||||||
|
'max_tokens' => $llmConfig['max_tokens'] ?? 500,
|
||||||
|
'temperature' => $llmConfig['temperature'] ?? 0.7
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perplexity
|
||||||
|
if (!empty($apiKeys['perplexity'])) {
|
||||||
|
$this->providers['perplexity'] = new PerplexityProvider([
|
||||||
|
'api_key' => $apiKeys['perplexity'],
|
||||||
|
'timeout' => $llmConfig['timeout'] ?? 30,
|
||||||
|
'max_tokens' => $llmConfig['max_tokens'] ?? 500,
|
||||||
|
'temperature' => $llmConfig['temperature'] ?? 0.7
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get provider by name
|
||||||
|
*/
|
||||||
|
public function getProvider(string $name): LLMProviderInterface
|
||||||
|
{
|
||||||
|
if (!isset($this->providers[$name])) {
|
||||||
|
throw new RuntimeException("LLM provider '{$name}' not available or not configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->providers[$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default provider
|
||||||
|
*/
|
||||||
|
public function getDefaultProvider(): LLMProviderInterface
|
||||||
|
{
|
||||||
|
return $this->getProvider($this->defaultProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate text using specified or default provider
|
||||||
|
*/
|
||||||
|
public function generateText(
|
||||||
|
string $prompt,
|
||||||
|
?string $provider = null,
|
||||||
|
array $options = []
|
||||||
|
): string {
|
||||||
|
$llm = $provider ? $this->getProvider($provider) : $this->getDefaultProvider();
|
||||||
|
return $llm->generateText($prompt, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of available providers
|
||||||
|
*/
|
||||||
|
public function getAvailableProviders(): array
|
||||||
|
{
|
||||||
|
$available = [];
|
||||||
|
foreach ($this->providers as $name => $provider) {
|
||||||
|
if ($provider->isAvailable()) {
|
||||||
|
$available[$name] = [
|
||||||
|
'name' => $name,
|
||||||
|
'display_name' => ucfirst($name),
|
||||||
|
'models' => $provider->getAvailableModels(),
|
||||||
|
'default_model' => $provider->getDefaultModel()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $available;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if any provider is available
|
||||||
|
*/
|
||||||
|
public function hasAvailableProvider(): bool
|
||||||
|
{
|
||||||
|
foreach ($this->providers as $provider) {
|
||||||
|
if ($provider->isAvailable()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default provider name
|
||||||
|
*/
|
||||||
|
public function getDefaultProviderName(): string
|
||||||
|
{
|
||||||
|
return $this->defaultProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\LLM;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM Provider Interface
|
||||||
|
*
|
||||||
|
* Defines the contract that all LLM provider implementations must follow
|
||||||
|
*/
|
||||||
|
interface LLMProviderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get provider name
|
||||||
|
*/
|
||||||
|
public function getName(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate text completion from prompt
|
||||||
|
*
|
||||||
|
* @param string $prompt User prompt
|
||||||
|
* @param array $options Additional options (model, temperature, max_tokens, etc.)
|
||||||
|
* @return string Generated text
|
||||||
|
* @throws \RuntimeException If API call fails
|
||||||
|
*/
|
||||||
|
public function generateText(string $prompt, array $options = []): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if provider is configured and available
|
||||||
|
*/
|
||||||
|
public function isAvailable(): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default model for this provider
|
||||||
|
*/
|
||||||
|
public function getDefaultModel(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available models for this provider
|
||||||
|
*/
|
||||||
|
public function getAvailableModels(): array;
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\LLM\Providers;
|
||||||
|
|
||||||
|
use App\Services\LLM\AbstractLLMProvider;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claude Provider (Anthropic)
|
||||||
|
*/
|
||||||
|
class ClaudeProvider extends AbstractLLMProvider
|
||||||
|
{
|
||||||
|
private const API_URL = 'https://api.anthropic.com/v1/messages';
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return 'claude';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDefaultModel(): string
|
||||||
|
{
|
||||||
|
return 'claude-3-sonnet-20240229';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAvailableModels(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'claude-3-opus-20240229' => 'Claude 3 Opus',
|
||||||
|
'claude-3-sonnet-20240229' => 'Claude 3 Sonnet',
|
||||||
|
'claude-3-haiku-20240307' => 'Claude 3 Haiku'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateText(string $prompt, array $options = []): string
|
||||||
|
{
|
||||||
|
if (!$this->isAvailable()) {
|
||||||
|
throw new RuntimeException('Claude API key not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
$model = $options['model'] ?? $this->getDefaultModel();
|
||||||
|
$temperature = $options['temperature'] ?? $this->temperature;
|
||||||
|
$maxTokens = $options['max_tokens'] ?? $this->maxTokens;
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'model' => $model,
|
||||||
|
'max_tokens' => $maxTokens,
|
||||||
|
'temperature' => $temperature,
|
||||||
|
'messages' => [
|
||||||
|
[
|
||||||
|
'role' => 'user',
|
||||||
|
'content' => $prompt
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'x-api-key' => $this->apiKey,
|
||||||
|
'anthropic-version' => '2023-06-01'
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->makeRequest(self::API_URL, $headers, $data);
|
||||||
|
|
||||||
|
if (!isset($response['content'][0]['text'])) {
|
||||||
|
throw new RuntimeException('Invalid Claude API response format');
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($response['content'][0]['text']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\LLM\Providers;
|
||||||
|
|
||||||
|
use App\Services\LLM\AbstractLLMProvider;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gemini Provider (Google)
|
||||||
|
*/
|
||||||
|
class GeminiProvider extends AbstractLLMProvider
|
||||||
|
{
|
||||||
|
private const API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent';
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return 'gemini';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDefaultModel(): string
|
||||||
|
{
|
||||||
|
return 'gemini-pro';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAvailableModels(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'gemini-pro' => 'Gemini Pro',
|
||||||
|
'gemini-1.5-pro' => 'Gemini 1.5 Pro',
|
||||||
|
'gemini-1.5-flash' => 'Gemini 1.5 Flash'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateText(string $prompt, array $options = []): string
|
||||||
|
{
|
||||||
|
if (!$this->isAvailable()) {
|
||||||
|
throw new RuntimeException('Gemini API key not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
$model = $options['model'] ?? $this->getDefaultModel();
|
||||||
|
$temperature = $options['temperature'] ?? $this->temperature;
|
||||||
|
$maxTokens = $options['max_tokens'] ?? $this->maxTokens;
|
||||||
|
|
||||||
|
$url = str_replace('{model}', $model, self::API_URL) . "?key={$this->apiKey}";
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'contents' => [
|
||||||
|
[
|
||||||
|
'parts' => [
|
||||||
|
['text' => $prompt]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'generationConfig' => [
|
||||||
|
'temperature' => $temperature,
|
||||||
|
'maxOutputTokens' => $maxTokens
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'Content-Type' => 'application/json'
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->makeRequest($url, $headers, $data);
|
||||||
|
|
||||||
|
if (!isset($response['candidates'][0]['content']['parts'][0]['text'])) {
|
||||||
|
throw new RuntimeException('Invalid Gemini API response format');
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($response['candidates'][0]['content']['parts'][0]['text']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\LLM\Providers;
|
||||||
|
|
||||||
|
use App\Services\LLM\AbstractLLMProvider;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenAI Provider (GPT models)
|
||||||
|
*/
|
||||||
|
class OpenAIProvider extends AbstractLLMProvider
|
||||||
|
{
|
||||||
|
private const API_URL = 'https://api.openai.com/v1/chat/completions';
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return 'openai';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDefaultModel(): string
|
||||||
|
{
|
||||||
|
return 'gpt-3.5-turbo';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAvailableModels(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'gpt-3.5-turbo' => 'GPT-3.5 Turbo',
|
||||||
|
'gpt-4' => 'GPT-4',
|
||||||
|
'gpt-4-turbo' => 'GPT-4 Turbo',
|
||||||
|
'gpt-4o' => 'GPT-4 Omni'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateText(string $prompt, array $options = []): string
|
||||||
|
{
|
||||||
|
if (!$this->isAvailable()) {
|
||||||
|
throw new RuntimeException('OpenAI API key not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
$model = $options['model'] ?? $this->getDefaultModel();
|
||||||
|
$temperature = $options['temperature'] ?? $this->temperature;
|
||||||
|
$maxTokens = $options['max_tokens'] ?? $this->maxTokens;
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'model' => $model,
|
||||||
|
'messages' => [
|
||||||
|
[
|
||||||
|
'role' => 'user',
|
||||||
|
'content' => $prompt
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'max_tokens' => $maxTokens,
|
||||||
|
'temperature' => $temperature
|
||||||
|
];
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'Authorization' => "Bearer {$this->apiKey}"
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->makeRequest(self::API_URL, $headers, $data);
|
||||||
|
|
||||||
|
if (!isset($response['choices'][0]['message']['content'])) {
|
||||||
|
throw new RuntimeException('Invalid OpenAI API response format');
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($response['choices'][0]['message']['content']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\LLM\Providers;
|
||||||
|
|
||||||
|
use App\Services\LLM\AbstractLLMProvider;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perplexity Provider
|
||||||
|
*/
|
||||||
|
class PerplexityProvider extends AbstractLLMProvider
|
||||||
|
{
|
||||||
|
private const API_URL = 'https://api.perplexity.ai/chat/completions';
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return 'perplexity';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDefaultModel(): string
|
||||||
|
{
|
||||||
|
return 'llama-3-sonar-small-32k-online';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAvailableModels(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'llama-3-sonar-small-32k-online' => 'Llama 3 Sonar Small (32k)',
|
||||||
|
'llama-3-sonar-large-32k-online' => 'Llama 3 Sonar Large (32k)',
|
||||||
|
'llama-3-8b-instruct' => 'Llama 3 8B Instruct',
|
||||||
|
'llama-3-70b-instruct' => 'Llama 3 70B Instruct'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateText(string $prompt, array $options = []): string
|
||||||
|
{
|
||||||
|
if (!$this->isAvailable()) {
|
||||||
|
throw new RuntimeException('Perplexity API key not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
$model = $options['model'] ?? $this->getDefaultModel();
|
||||||
|
$temperature = $options['temperature'] ?? $this->temperature;
|
||||||
|
$maxTokens = $options['max_tokens'] ?? $this->maxTokens;
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'model' => $model,
|
||||||
|
'messages' => [
|
||||||
|
[
|
||||||
|
'role' => 'user',
|
||||||
|
'content' => $prompt
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'max_tokens' => $maxTokens,
|
||||||
|
'temperature' => $temperature
|
||||||
|
];
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'Authorization' => "Bearer {$this->apiKey}"
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->makeRequest(self::API_URL, $headers, $data);
|
||||||
|
|
||||||
|
if (!isset($response['choices'][0]['message']['content'])) {
|
||||||
|
throw new RuntimeException('Invalid Perplexity API response format');
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($response['choices'][0]['message']['content']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Logging;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LoggingHelper - Centralized logging utility
|
||||||
|
*
|
||||||
|
* Provides static methods for logging with automatic null-safety and
|
||||||
|
* context injection. Eliminates the need for repeated if statements
|
||||||
|
* checking logger availability and log levels.
|
||||||
|
*
|
||||||
|
* All logging methods:
|
||||||
|
* - Accept optional logger (null-safe)
|
||||||
|
* - Inject generator context automatically
|
||||||
|
* - Support additional context data
|
||||||
|
* - Are static (no instantiation needed)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* LoggingHelper::debug($this->logger, $this->getId(), 'Starting generation');
|
||||||
|
* LoggingHelper::info($this->logger, 'DicewareGenerator', 'Complete', ['count' => 5]);
|
||||||
|
* LoggingHelper::warning($this->logger, 'Pwgen', 'Using fallback', $context);
|
||||||
|
* LoggingHelper::error($this->logger, 'XkcdPass', 'Failed', ['error' => $e->getMessage()]);
|
||||||
|
*
|
||||||
|
* @package App\Services\Logging
|
||||||
|
*/
|
||||||
|
class LoggingHelper
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* PSR-3 severity levels for reference
|
||||||
|
* Lower numbers = more verbose (more messages logged)
|
||||||
|
* Higher numbers = less verbose (fewer messages logged)
|
||||||
|
*/
|
||||||
|
private const LOG_LEVELS = [
|
||||||
|
'debug' => 0, // Most verbose - development messages
|
||||||
|
'info' => 1, // Informational - important milestones
|
||||||
|
'notice' => 2, // Normal but significant
|
||||||
|
'warning' => 3, // Warning - potential issues
|
||||||
|
'error' => 4, // Error - something went wrong
|
||||||
|
'critical' => 5, // Critical - system in trouble
|
||||||
|
'alert' => 6, // Alert - immediate action needed
|
||||||
|
'emergency' => 7, // Emergency - system unusable
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a DEBUG message
|
||||||
|
*
|
||||||
|
* DEBUG level is for detailed development information.
|
||||||
|
* Only logged when LOG_LEVEL is set to 'debug'.
|
||||||
|
*
|
||||||
|
* @param LoggerInterface|null $logger Logger instance (null-safe)
|
||||||
|
* @param string $generatorId Generator identifier for context
|
||||||
|
* @param string $message The log message
|
||||||
|
* @param array $context Additional contextual data
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* LoggingHelper::debug($logger, 'DicewareGenerator', 'Starting generation', [
|
||||||
|
* 'count' => 5,
|
||||||
|
* 'words' => 6
|
||||||
|
* ]);
|
||||||
|
*/
|
||||||
|
public static function debug(
|
||||||
|
?LoggerInterface $logger,
|
||||||
|
string $generatorId,
|
||||||
|
string $message,
|
||||||
|
array $context = []
|
||||||
|
): void {
|
||||||
|
self::logWithContext($logger, 'debug', $generatorId, $message, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log an INFO message
|
||||||
|
*
|
||||||
|
* INFO level is for informational messages about important events.
|
||||||
|
* Logged when LOG_LEVEL is 'debug' or 'info' or higher.
|
||||||
|
*
|
||||||
|
* @param LoggerInterface|null $logger Logger instance (null-safe)
|
||||||
|
* @param string $generatorId Generator identifier for context
|
||||||
|
* @param string $message The log message
|
||||||
|
* @param array $context Additional contextual data
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* LoggingHelper::info($logger, 'DicewareGenerator', 'Generation complete', [
|
||||||
|
* 'count' => 5,
|
||||||
|
* 'duration_ms' => 145
|
||||||
|
* ]);
|
||||||
|
*/
|
||||||
|
public static function info(
|
||||||
|
?LoggerInterface $logger,
|
||||||
|
string $generatorId,
|
||||||
|
string $message,
|
||||||
|
array $context = []
|
||||||
|
): void {
|
||||||
|
self::logWithContext($logger, 'info', $generatorId, $message, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a WARNING message
|
||||||
|
*
|
||||||
|
* WARNING level is for potentially problematic situations.
|
||||||
|
* Logged when LOG_LEVEL is 'warning' or higher (less verbose).
|
||||||
|
*
|
||||||
|
* @param LoggerInterface|null $logger Logger instance (null-safe)
|
||||||
|
* @param string $generatorId Generator identifier for context
|
||||||
|
* @param string $message The log message
|
||||||
|
* @param array $context Additional contextual data
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* LoggingHelper::warning($logger, 'Pwgen', 'Command not found, using fallback', [
|
||||||
|
* 'command' => 'pwgen',
|
||||||
|
* 'fallback' => 'php_random'
|
||||||
|
* ]);
|
||||||
|
*/
|
||||||
|
public static function warning(
|
||||||
|
?LoggerInterface $logger,
|
||||||
|
string $generatorId,
|
||||||
|
string $message,
|
||||||
|
array $context = []
|
||||||
|
): void {
|
||||||
|
self::logWithContext($logger, 'warning', $generatorId, $message, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log an ERROR message
|
||||||
|
*
|
||||||
|
* ERROR level is for error conditions.
|
||||||
|
* Always logged (unless logger is null).
|
||||||
|
*
|
||||||
|
* @param LoggerInterface|null $logger Logger instance (null-safe)
|
||||||
|
* @param string $generatorId Generator identifier for context
|
||||||
|
* @param string $message The log message
|
||||||
|
* @param array $context Additional contextual data
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* LoggingHelper::error($logger, 'XkcdPass', 'Generation failed', [
|
||||||
|
* 'error' => 'Invalid word count',
|
||||||
|
* 'provided_count' => 200
|
||||||
|
* ]);
|
||||||
|
*/
|
||||||
|
public static function error(
|
||||||
|
?LoggerInterface $logger,
|
||||||
|
string $generatorId,
|
||||||
|
string $message,
|
||||||
|
array $context = []
|
||||||
|
): void {
|
||||||
|
self::logWithContext($logger, 'error', $generatorId, $message, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a CRITICAL message
|
||||||
|
*
|
||||||
|
* CRITICAL level is for critical errors.
|
||||||
|
* Always logged (unless logger is null).
|
||||||
|
*
|
||||||
|
* @param LoggerInterface|null $logger Logger instance (null-safe)
|
||||||
|
* @param string $generatorId Generator identifier for context
|
||||||
|
* @param string $message The log message
|
||||||
|
* @param array $context Additional contextual data
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* LoggingHelper::critical($logger, 'PasswordManager', 'Configuration loading failed');
|
||||||
|
*/
|
||||||
|
public static function critical(
|
||||||
|
?LoggerInterface $logger,
|
||||||
|
string $generatorId,
|
||||||
|
string $message,
|
||||||
|
array $context = []
|
||||||
|
): void {
|
||||||
|
self::logWithContext($logger, 'critical', $generatorId, $message, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core logging implementation with context injection
|
||||||
|
*
|
||||||
|
* This method:
|
||||||
|
* 1. Checks if logger is available (null-safe)
|
||||||
|
* 2. Injects generator context into all messages
|
||||||
|
* 3. Routes to appropriate PSR-3 log method
|
||||||
|
*
|
||||||
|
* @param LoggerInterface|null $logger Logger instance
|
||||||
|
* @param string $level Log level (debug, info, warning, error, critical)
|
||||||
|
* @param string $generatorId Generator identifier for context
|
||||||
|
* @param string $message Log message
|
||||||
|
* @param array $context Additional context data
|
||||||
|
*/
|
||||||
|
private static function logWithContext(
|
||||||
|
?LoggerInterface $logger,
|
||||||
|
string $level,
|
||||||
|
string $generatorId,
|
||||||
|
string $message,
|
||||||
|
array $context = []
|
||||||
|
): void {
|
||||||
|
// Guard: No logger available - silently return
|
||||||
|
if ($logger === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject generator context into all messages
|
||||||
|
$fullContext = array_merge(
|
||||||
|
['generator' => $generatorId],
|
||||||
|
$context
|
||||||
|
);
|
||||||
|
|
||||||
|
// Route to appropriate PSR-3 logging method
|
||||||
|
match ($level) {
|
||||||
|
'debug' => $logger->debug($message, $fullContext),
|
||||||
|
'info' => $logger->info($message, $fullContext),
|
||||||
|
'notice' => $logger->notice($message, $fullContext),
|
||||||
|
'warning' => $logger->warning($message, $fullContext),
|
||||||
|
'error' => $logger->error($message, $fullContext),
|
||||||
|
'critical' => $logger->critical($message, $fullContext),
|
||||||
|
'alert' => $logger->alert($message, $fullContext),
|
||||||
|
'emergency' => $logger->emergency($message, $fullContext),
|
||||||
|
default => $logger->info($message, $fullContext)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if logging at a specific level is enabled
|
||||||
|
*
|
||||||
|
* Useful when building expensive log messages (e.g., serialization, formatting).
|
||||||
|
* Allows conditional construction of message data.
|
||||||
|
*
|
||||||
|
* Note: This is a reference helper. Most PSR-3 loggers handle this internally.
|
||||||
|
*
|
||||||
|
* @param string $configuredLevel Current configured log level from config
|
||||||
|
* @param string $checkLevel Level to check
|
||||||
|
* @return bool True if message at checkLevel would be logged
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* if (LoggingHelper::isLevelEnabled($configLevel, 'debug')) {
|
||||||
|
* $expensiveDebugData = buildComplexDebugInfo();
|
||||||
|
* LoggingHelper::debug($logger, $id, 'Data', ['debug' => $expensiveDebugData]);
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public static function isLevelEnabled(
|
||||||
|
string $configuredLevel,
|
||||||
|
string $checkLevel
|
||||||
|
): bool {
|
||||||
|
$configValue = self::LOG_LEVELS[$configuredLevel] ?? self::LOG_LEVELS['info'];
|
||||||
|
$checkValue = self::LOG_LEVELS[$checkLevel] ?? self::LOG_LEVELS['info'];
|
||||||
|
|
||||||
|
return $checkValue >= $configValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* USAGE EXAMPLES
|
||||||
|
* ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Example 1: In AbstractPasswordGenerator.php
|
||||||
|
/*
|
||||||
|
protected function loadConfiguration(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->config = json_decode(file_get_contents('config/config.json'), true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
LoggingHelper::debug($this->logger, $this->getId(), 'Configuration loaded');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
LoggingHelper::warning($this->logger, $this->getId(),
|
||||||
|
'Failed to load config', ['error' => $e->getMessage()]);
|
||||||
|
$this->config = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Example 2: In DicewareGenerator.php
|
||||||
|
/*
|
||||||
|
public function generate(int $count = 1, array $options = []): array
|
||||||
|
{
|
||||||
|
$options = $this->validateOptions($options);
|
||||||
|
|
||||||
|
LoggingHelper::debug($this->logger, $this->getId(),
|
||||||
|
'Starting generation', ['count' => $count]);
|
||||||
|
|
||||||
|
$passwords = [];
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
$words = $this->selectRandomWords($options['words']);
|
||||||
|
$passwords[] = implode('-', $words);
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggingHelper::info($this->logger, $this->getId(),
|
||||||
|
'Generation complete', ['count' => count($passwords)]);
|
||||||
|
|
||||||
|
return $passwords;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Example 3: In PwgenGenerator.php
|
||||||
|
/*
|
||||||
|
private function generateWithNativePwgen(int $length, int $count): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$command = "pwgen -S {$length} {$count}";
|
||||||
|
$result = shell_exec(escapeshellcmd($command));
|
||||||
|
|
||||||
|
if ($result === null) {
|
||||||
|
throw new RuntimeException('PWGen command failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggingHelper::debug($this->logger, $this->getId(),
|
||||||
|
'Native pwgen generated', ['count' => $count]);
|
||||||
|
|
||||||
|
return array_filter(explode("\n", trim($result)));
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
LoggingHelper::warning($this->logger, $this->getId(),
|
||||||
|
'Native pwgen failed, using fallback', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'fallback' => 'php_random'
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw new RuntimeException(
|
||||||
|
"PWGen execution failed: " . $e->getMessage(),
|
||||||
|
0,
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Example 4: In XkcdPassGenerator.php
|
||||||
|
/*
|
||||||
|
private function generateAiWordlist(int $wordCount, string $prompt): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
LoggingHelper::debug($this->logger, $this->getId(),
|
||||||
|
'Requesting AI wordlist', ['word_count' => $wordCount]);
|
||||||
|
|
||||||
|
$response = $this->llmManager->generateText($prompt);
|
||||||
|
$words = $this->parseWordList($response, $wordCount);
|
||||||
|
|
||||||
|
if (count($words) < $wordCount) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"LLM returned only " . count($words) . " words"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggingHelper::info($this->logger, $this->getId(),
|
||||||
|
'AI wordlist generated', ['count' => count($words)]);
|
||||||
|
|
||||||
|
return $words;
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
LoggingHelper::error($this->logger, $this->getId(),
|
||||||
|
'AI wordlist generation failed', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'requested_count' => $wordCount
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Failed to generate wordlist: " . $e->getMessage(),
|
||||||
|
0,
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
?>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Utils;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use JsonException;
|
||||||
|
|
||||||
|
class ConfigLoader
|
||||||
|
{
|
||||||
|
public static function load(?string $configPath = null): array
|
||||||
|
{
|
||||||
|
$path = $configPath ?? dirname(__DIR__, 2) . '/config/config.json';
|
||||||
|
|
||||||
|
if (!is_file($path)) {
|
||||||
|
throw new RuntimeException('Configuration file not found at ' . $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = file_get_contents($path);
|
||||||
|
if ($json === false) {
|
||||||
|
throw new RuntimeException('Unable to read configuration file at ' . $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$decoded = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
} catch (JsonException $e) {
|
||||||
|
throw new RuntimeException('Invalid JSON in configuration file at ' . $path, 0, $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
throw new RuntimeException('Configuration root must decode to an object at ' . $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Utils;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
|
||||||
|
class ResponseHelper
|
||||||
|
{
|
||||||
|
public function successResponse(
|
||||||
|
Response $response,
|
||||||
|
array $data = [],
|
||||||
|
?string $message = null // FIXED: Explicit nullable type
|
||||||
|
): Response {
|
||||||
|
$responseData = [
|
||||||
|
'success' => true,
|
||||||
|
'data' => $data,
|
||||||
|
'timestamp' => date('c')
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($message !== null) {
|
||||||
|
$responseData['message'] = $message;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response->getBody()->write(json_encode($responseData, JSON_UNESCAPED_UNICODE));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function errorResponse(
|
||||||
|
Response $response,
|
||||||
|
string $message,
|
||||||
|
int $statusCode = 400,
|
||||||
|
?string $errorCode = null
|
||||||
|
): Response {
|
||||||
|
$responseData = [
|
||||||
|
'success' => false,
|
||||||
|
'error' => [
|
||||||
|
'message' => $message,
|
||||||
|
'code' => $errorCode ?? 'ERROR',
|
||||||
|
'status' => $statusCode
|
||||||
|
],
|
||||||
|
'timestamp' => date('c')
|
||||||
|
];
|
||||||
|
|
||||||
|
$response->getBody()->write(json_encode($responseData, JSON_UNESCAPED_UNICODE));
|
||||||
|
return $response
|
||||||
|
->withHeader('Content-Type', 'application/json')
|
||||||
|
->withStatus($statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function jsonResponse(Response $response, array $data, int $statusCode = 200): Response
|
||||||
|
{
|
||||||
|
$response->getBody()->write(json_encode($data, JSON_UNESCAPED_UNICODE));
|
||||||
|
return $response
|
||||||
|
->withHeader('Content-Type', 'application/json')
|
||||||
|
->withStatus($statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Utils;
|
||||||
|
|
||||||
|
use Monolog\Logger;
|
||||||
|
|
||||||
|
final class SecurityConfig
|
||||||
|
{
|
||||||
|
public static function resolveCorsOrigin(?string $environment = null): string
|
||||||
|
{
|
||||||
|
$configuredOrigin = trim((string) ($_ENV['CORS_ORIGIN'] ?? ''));
|
||||||
|
if ($configuredOrigin !== '') {
|
||||||
|
return $configuredOrigin;
|
||||||
|
}
|
||||||
|
|
||||||
|
$env = strtolower((string) ($environment ?? ($_ENV['APP_ENV'] ?? 'production')));
|
||||||
|
|
||||||
|
if (in_array($env, ['development', 'local', 'dev'], true)) {
|
||||||
|
return 'http://localhost:8080';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function resolveRateLimit(?string $environment = null, int $fallback = 120): int
|
||||||
|
{
|
||||||
|
if (isset($_ENV['RATE_LIMIT']) && is_numeric($_ENV['RATE_LIMIT'])) {
|
||||||
|
return (int) $_ENV['RATE_LIMIT'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return max(1, $fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function resolveLogLevel(?string $configuredLevel = null): int
|
||||||
|
{
|
||||||
|
$level = strtoupper((string) ($configuredLevel ?? ($_ENV['LOG_LEVEL'] ?? 'warning')));
|
||||||
|
|
||||||
|
return match ($level) {
|
||||||
|
'DEBUG' => Logger::DEBUG,
|
||||||
|
'INFO' => Logger::INFO,
|
||||||
|
'NOTICE' => Logger::NOTICE,
|
||||||
|
'WARNING' => Logger::WARNING,
|
||||||
|
'ERROR' => Logger::ERROR,
|
||||||
|
'CRITICAL' => Logger::CRITICAL,
|
||||||
|
'ALERT' => Logger::ALERT,
|
||||||
|
'EMERGENCY' => Logger::EMERGENCY,
|
||||||
|
default => Logger::WARNING,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title data-i18n="app_title">Password Generator</title>
|
||||||
|
<link rel="stylesheet" href="../assets/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="header">
|
||||||
|
<div class="container">
|
||||||
|
<h1>🔐 <span data-i18n="app_title">Password Generator</span></h1>
|
||||||
|
<a href="/docs">📚 <span data-i18n="api_documentation">API Documentation</a></a>
|
||||||
|
|
||||||
|
<div class="language-selector">
|
||||||
|
<label for="language-select" data-i18n="language">Language:</label>
|
||||||
|
<select id="language-select">
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="de">Deutsch</option>
|
||||||
|
<option value="de_CH">Schwyzerdütsch</option>
|
||||||
|
<option value="fr">Français</option>
|
||||||
|
<option value="it">Italiano</option>
|
||||||
|
<option value="es">Español</option>
|
||||||
|
<option value="pt">Portugues</option>
|
||||||
|
<option value="ja">日本語</option>
|
||||||
|
<option value="zh">中文</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main-container">
|
||||||
|
<div class="loading" id="initial-loading">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<p data-i18n="loading_generators">Loading password generators...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="generators-container" class="generators-grid" style="display: none;">
|
||||||
|
<!-- Password generator cards will be inserted here by JavaScript -->
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<p>
|
||||||
|
<span data-i18n="footer_text">Footer Text</span>
|
||||||
|
<a href="/docs" data-i18n="api_documentation">API Documentation</a>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="/assets/js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Generators\HighEntropyGenerator;
|
||||||
|
use App\Generators\PasswordGeneratorInterface;
|
||||||
|
use App\Generators\PhraseAbbreviatorGenerator;
|
||||||
|
use App\Generators\PwgenGenerator;
|
||||||
|
use App\Managers\PasswordGeneratorManager;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class GeneratorValidationTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testDefaultGenerationWorksAcrossSupportedModes(): void
|
||||||
|
{
|
||||||
|
$manager = new PasswordGeneratorManager();
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
'high-entropy',
|
||||||
|
'xkcdpass',
|
||||||
|
'diceware',
|
||||||
|
'pwgen',
|
||||||
|
'passkey',
|
||||||
|
'phrase-abbreviator',
|
||||||
|
] as $type) {
|
||||||
|
$defaults = $manager->getDefaultOptions($type);
|
||||||
|
$result = $manager->generate($type, 1, $defaults);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('passwords', $result);
|
||||||
|
$this->assertNotEmpty($result['passwords']);
|
||||||
|
$this->assertNotSame('', trim((string) $result['passwords'][0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHighEntropyRejectsAllFalseCharacterSets(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\RuntimeException::class);
|
||||||
|
|
||||||
|
(new HighEntropyGenerator())->generate(1, [
|
||||||
|
'length' => 16,
|
||||||
|
'include_lowercase' => false,
|
||||||
|
'include_uppercase' => false,
|
||||||
|
'include_numbers' => false,
|
||||||
|
'include_symbols' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPhraseAbbreviatorUsesFallbackSentenceWhenMissing(): void
|
||||||
|
{
|
||||||
|
$generator = new PhraseAbbreviatorGenerator();
|
||||||
|
|
||||||
|
$validated = $generator->validateOptions(['sentence' => '']);
|
||||||
|
|
||||||
|
$this->assertNotSame('', $validated['sentence']);
|
||||||
|
$this->assertSame('The quick brown fox jumps over the lazy dog.', $validated['sentence']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGeneratorDefinitionsMatchFrontendMetadataContract(): void
|
||||||
|
{
|
||||||
|
$manager = new PasswordGeneratorManager();
|
||||||
|
$definitions = $manager->getGeneratorDefinitions();
|
||||||
|
|
||||||
|
$this->assertNotEmpty($definitions);
|
||||||
|
$this->assertArrayHasKey('high-entropy', $definitions);
|
||||||
|
|
||||||
|
$highEntropy = $definitions['high-entropy'];
|
||||||
|
$this->assertSame('high-entropy_title', $highEntropy['nameKey']);
|
||||||
|
$this->assertSame('high-entropy_description', $highEntropy['descriptionKey']);
|
||||||
|
$this->assertNotEmpty($highEntropy['options']);
|
||||||
|
$this->assertSame('high-entropy_option_length', $highEntropy['options'][0]['labelKey']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGeneratedPasswordsExposeSecurityAssessmentAndEntropyDetails(): void
|
||||||
|
{
|
||||||
|
$manager = new PasswordGeneratorManager();
|
||||||
|
$result = $manager->generate('diceware', 1, [
|
||||||
|
'words' => 6,
|
||||||
|
'show_dice_rolls' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('passwords', $result);
|
||||||
|
$this->assertArrayHasKey('security_assessment', $result);
|
||||||
|
$this->assertNotSame('', trim((string) $result['security_assessment']));
|
||||||
|
$this->assertArrayHasKey('raw_technical_details', $result);
|
||||||
|
$this->assertArrayHasKey('entropy_bits', $result['raw_technical_details']);
|
||||||
|
$this->assertGreaterThan(0, $result['raw_technical_details']['entropy_bits']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPwgenGeneratorDoesNotExposeRemovedTypePresetOption(): void
|
||||||
|
{
|
||||||
|
$generator = new PwgenGenerator();
|
||||||
|
$options = $generator->getOptionDefinitions();
|
||||||
|
|
||||||
|
$this->assertArrayNotHasKey('type', $options);
|
||||||
|
$this->assertSame('custom', $generator->validateOptions([])['type'] ?? 'custom');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testXkcdEntropyReflectsWordLengthFiltering(): void
|
||||||
|
{
|
||||||
|
$manager = new PasswordGeneratorManager();
|
||||||
|
|
||||||
|
$unfiltered = $manager->generate('xkcdpass', 1, [
|
||||||
|
'words' => 4,
|
||||||
|
'wordlist' => 'en',
|
||||||
|
'separator' => '-',
|
||||||
|
'max_word_length' => 0,
|
||||||
|
'space_handling' => 'allow',
|
||||||
|
'add_number' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$shortFiltered = $manager->generate('xkcdpass', 1, [
|
||||||
|
'words' => 4,
|
||||||
|
'wordlist' => 'en',
|
||||||
|
'separator' => '-',
|
||||||
|
'max_word_length' => 4,
|
||||||
|
'space_handling' => 'allow',
|
||||||
|
'add_number' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertGreaterThan($shortFiltered['entropy_bits'], $unfiltered['entropy_bits']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testXkcdResponseIncludesEntropyBreakdownDetails(): void
|
||||||
|
{
|
||||||
|
$manager = new PasswordGeneratorManager();
|
||||||
|
$result = $manager->generate('xkcdpass', 1, [
|
||||||
|
'words' => 4,
|
||||||
|
'wordlist' => 'en',
|
||||||
|
'separator' => '-_',
|
||||||
|
'max_word_length' => 6,
|
||||||
|
'space_handling' => 'skip',
|
||||||
|
'add_number' => true,
|
||||||
|
'number_position' => 'random',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('raw_technical_details', $result);
|
||||||
|
$this->assertArrayHasKey('entropy_breakdown', $result['raw_technical_details']);
|
||||||
|
|
||||||
|
$breakdown = $result['raw_technical_details']['entropy_breakdown'];
|
||||||
|
$this->assertSame('xkcd-candidate-space-v1', $breakdown['entropy_model']);
|
||||||
|
$this->assertArrayHasKey('effective_wordlist_candidates', $breakdown);
|
||||||
|
$this->assertGreaterThan(0, $breakdown['effective_wordlist_candidates']);
|
||||||
|
$this->assertArrayHasKey('separator_entropy_bits', $breakdown);
|
||||||
|
$this->assertArrayHasKey('number_entropy_bits', $breakdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGeneratorsExposeEntropyModelConfidenceInBreakdown(): void
|
||||||
|
{
|
||||||
|
$manager = new PasswordGeneratorManager();
|
||||||
|
|
||||||
|
$cases = [
|
||||||
|
'diceware' => ['words' => 6],
|
||||||
|
'passkey' => ['syllables' => 6],
|
||||||
|
'pwgen' => ['length' => 12, 'separator' => '-_', 'include_numbers' => true, 'include_capitals' => true],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($cases as $type => $options) {
|
||||||
|
$result = $manager->generate($type, 1, $options);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('raw_technical_details', $result);
|
||||||
|
$this->assertArrayHasKey('entropy_breakdown', $result['raw_technical_details']);
|
||||||
|
$this->assertArrayHasKey('entropy_model', $result['raw_technical_details']['entropy_breakdown']);
|
||||||
|
$this->assertArrayHasKey('model_confidence', $result['raw_technical_details']['entropy_breakdown']);
|
||||||
|
$this->assertContains(
|
||||||
|
$result['raw_technical_details']['entropy_breakdown']['model_confidence'],
|
||||||
|
['high', 'medium', 'low']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPwgenAndXkcdCurrentOptionsProduceExpectedBehavior(): void
|
||||||
|
{
|
||||||
|
$manager = new PasswordGeneratorManager();
|
||||||
|
|
||||||
|
$pwgen16 = (string) $manager->generate('pwgen', 1, ['length' => 16])['passwords'][0];
|
||||||
|
$pwgen12 = (string) $manager->generate('pwgen', 1, ['length' => 12])['passwords'][0];
|
||||||
|
$pwgen8 = (string) $manager->generate('pwgen', 1, ['length' => 8])['passwords'][0];
|
||||||
|
|
||||||
|
$this->assertSame(16, strlen($pwgen16));
|
||||||
|
$this->assertSame(12, strlen($pwgen12));
|
||||||
|
$this->assertSame(8, strlen($pwgen8));
|
||||||
|
|
||||||
|
$xkcdGerman = (string) $manager->generate('xkcdpass', 1, [
|
||||||
|
'wordlist' => 'de',
|
||||||
|
'words' => 4,
|
||||||
|
'separator' => '-',
|
||||||
|
'add_number' => true,
|
||||||
|
'number_position' => 'end',
|
||||||
|
])['passwords'][0];
|
||||||
|
|
||||||
|
$xkcdEnglish = (string) $manager->generate('xkcdpass', 1, [
|
||||||
|
'wordlist' => 'en',
|
||||||
|
'words' => 4,
|
||||||
|
'separator' => '-',
|
||||||
|
'add_number' => true,
|
||||||
|
'number_position' => 'end',
|
||||||
|
])['passwords'][0];
|
||||||
|
|
||||||
|
$xkcdSlaengikon = (string) $manager->generate('xkcdpass', 1, [
|
||||||
|
'wordlist' => 'ch',
|
||||||
|
'words' => 4,
|
||||||
|
'separator' => '_',
|
||||||
|
'add_number' => true,
|
||||||
|
'number_position' => 'end',
|
||||||
|
])['passwords'][0];
|
||||||
|
|
||||||
|
$this->assertSame(1, preg_match('/\d/', $xkcdGerman));
|
||||||
|
$this->assertSame(1, preg_match('/\d/', $xkcdEnglish));
|
||||||
|
$this->assertSame(1, preg_match('/\d/', $xkcdSlaengikon));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDicewareUsesLargeEffectiveCandidateSpace(): void
|
||||||
|
{
|
||||||
|
$manager = new PasswordGeneratorManager();
|
||||||
|
$result = $manager->generate('diceware', 1, [
|
||||||
|
'words' => 6,
|
||||||
|
'show_dice_rolls' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('raw_technical_details', $result);
|
||||||
|
$this->assertArrayHasKey('entropy_breakdown', $result['raw_technical_details']);
|
||||||
|
$this->assertArrayHasKey('wordlist_size', $result['raw_technical_details']['entropy_breakdown']);
|
||||||
|
$this->assertGreaterThanOrEqual(7000, $result['raw_technical_details']['entropy_breakdown']['wordlist_size']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Api\OpenApiGenerator;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class OpenApiGeneratorTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testGenerateIncludesPublicApiRoutes(): void
|
||||||
|
{
|
||||||
|
$spec = OpenApiGenerator::generate('http://localhost:8080');
|
||||||
|
|
||||||
|
$this->assertSame('3.0.3', $spec['openapi']);
|
||||||
|
$this->assertArrayHasKey('/api/v1/passwords', $spec['paths']);
|
||||||
|
$this->assertArrayHasKey('/api/v1/passwords/generate', $spec['paths']);
|
||||||
|
$this->assertArrayHasKey('/api/v1/health', $spec['paths']);
|
||||||
|
$this->assertArrayHasKey('/api/v1/locales/{lang}', $spec['paths']);
|
||||||
|
$this->assertSame('http://localhost:8080', $spec['servers'][0]['url']);
|
||||||
|
|
||||||
|
$this->assertSame('boolean', $spec['paths']['/api/v1/passwords']['get']['responses']['200']['content']['application/json']['schema']['properties']['success']['type']);
|
||||||
|
$this->assertArrayHasKey('types', $spec['paths']['/api/v1/passwords']['get']['responses']['200']['content']['application/json']['schema']['properties']['data']['properties']);
|
||||||
|
$this->assertSame('#/components/schemas/PasswordGenerationSuccess', $spec['paths']['/api/v1/passwords/generate']['post']['responses']['200']['content']['application/json']['schema']['$ref']);
|
||||||
|
|
||||||
|
$passwordResponseData = $spec['components']['schemas']['PasswordGenerationSuccess']['properties']['data']['properties'];
|
||||||
|
$this->assertArrayHasKey('security_assessment', $passwordResponseData);
|
||||||
|
$this->assertArrayHasKey('raw_technical_details', $passwordResponseData);
|
||||||
|
$this->assertArrayHasKey('entropy_breakdown', $passwordResponseData['raw_technical_details']['properties']);
|
||||||
|
|
||||||
|
$localeEnum = $spec['paths']['/api/v1/locales/{lang}']['get']['parameters'][0]['schema']['enum'];
|
||||||
|
$this->assertContains('ja', $localeEnum);
|
||||||
|
$this->assertContains('de_CH', $localeEnum);
|
||||||
|
$this->assertNotContains('js', $localeEnum);
|
||||||
|
$this->assertNotContains('ch', $localeEnum);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Utils\SecurityConfig;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class ProductionSecurityConfigTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testDevelopmentDefaultsKeepLocalhostCorsAndDebugLogging(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('http://localhost:8080', SecurityConfig::resolveCorsOrigin('development'));
|
||||||
|
$this->assertSame(100, SecurityConfig::resolveRateLimit('development', 100));
|
||||||
|
$this->assertSame(100, SecurityConfig::resolveLogLevel('debug'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProductionDefaultsRejectWildcardOriginsAndUseRestrictedLogging(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('', SecurityConfig::resolveCorsOrigin('production'));
|
||||||
|
$this->assertSame(120, SecurityConfig::resolveRateLimit('production', 120));
|
||||||
|
$this->assertSame(300, SecurityConfig::resolveLogLevel('warning'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
// scripts/generate-docs.php
|
||||||
|
|
||||||
|
class DocumentationGenerator
|
||||||
|
{
|
||||||
|
private array $languages = [
|
||||||
|
'de' => 'Deutsch',
|
||||||
|
'fr' => 'Français',
|
||||||
|
'es' => 'Español',
|
||||||
|
'it' => 'Italiano',
|
||||||
|
'ja' => '日本語',
|
||||||
|
'zh' => '中文'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function generateAllLanguages(): void
|
||||||
|
{
|
||||||
|
$baseTemplate = file_get_contents(__DIR__ . '/../docs/index.html');
|
||||||
|
|
||||||
|
foreach ($this->languages as $code => $name) {
|
||||||
|
$translated = $this->translateContent($baseTemplate, $code);
|
||||||
|
file_put_contents(__DIR__ . "/../docs/docs-{$code}.html", $translated);
|
||||||
|
echo "Generated docs-{$code}.html\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function translateContent(string $content, string $lang): string
|
||||||
|
{
|
||||||
|
// Load translations from JSON files
|
||||||
|
$translations = json_decode(
|
||||||
|
file_get_contents(__DIR__ . "/../locales/{$lang}.json"),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Replace content with translations
|
||||||
|
$content = str_replace('lang="en"', "lang=\"{$lang}\"", $content);
|
||||||
|
|
||||||
|
// Replace specific content sections
|
||||||
|
foreach ($translations as $key => $value) {
|
||||||
|
$content = str_replace("{{$key}}", $value, $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
$generator = new DocumentationGenerator();
|
||||||
|
$generator->generateAllLanguages();
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
# Translation Management Tools
|
||||||
|
|
||||||
|
## 📝 Overview
|
||||||
|
|
||||||
|
This directory contains tools and utilities for managing translations across the Password Generator application. These tools help identify, extract, and maintain translatable strings across HTML, JavaScript, and PHP files.
|
||||||
|
|
||||||
|
## 🛠️ Tools Provided
|
||||||
|
|
||||||
|
### 1. **extract-translations.php**
|
||||||
|
Advanced PHP-based translation extractor that scans your codebase and automatically generates/updates translation files.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- ✅ Scans HTML, JavaScript, and PHP files
|
||||||
|
- ✅ Extracts `data-i18n` attributes
|
||||||
|
- ✅ Finds `this.t()` calls in JavaScript
|
||||||
|
- ✅ Identifies hardcoded strings
|
||||||
|
- ✅ Generates translation keys automatically
|
||||||
|
- ✅ Updates existing translation files
|
||||||
|
- ✅ Provides basic translations for common strings
|
||||||
|
- ✅ Preserves consistent key structure across locales
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
php extract-translations.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **update-translations.sh**
|
||||||
|
Shell script for quick translation file updates and scanning.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- ✅ Fast scanning with grep/sed
|
||||||
|
- ✅ Updates all language files at once
|
||||||
|
- ✅ Shows translation coverage statistics
|
||||||
|
- ✅ Identifies hardcoded strings
|
||||||
|
- ✅ Color-coded output
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
chmod +x update-translations.sh
|
||||||
|
./update-translations.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Current Translation Status
|
||||||
|
|
||||||
|
### Supported Languages
|
||||||
|
- 🇺🇸 **English (en)** - Base language
|
||||||
|
- 🇩🇪 **German (de)** - Partially translated
|
||||||
|
- 🇫🇷 **French (fr)** - Partially translated
|
||||||
|
- 🇮🇹 **Italian (it)** - Placeholders only
|
||||||
|
- 🇪🇸 **Spanish (es)** - Placeholders only
|
||||||
|
- 🇯🇵 **Japanese (ja)** - Placeholders only
|
||||||
|
- 🇨🇳 **Chinese (zh)** - Placeholders only
|
||||||
|
|
||||||
|
### Translation Keys Identified
|
||||||
|
|
||||||
|
The tools have identified these categories of translatable strings:
|
||||||
|
|
||||||
|
#### **UI Elements**
|
||||||
|
- `generate` - Generate button text
|
||||||
|
- `copy` - Copy button text
|
||||||
|
- `language` - Language selector label
|
||||||
|
- `options` - Options section header
|
||||||
|
- `loading` - Loading messages
|
||||||
|
- `error` - Error messages
|
||||||
|
- `success` - Success messages
|
||||||
|
|
||||||
|
#### **Password Generator Types**
|
||||||
|
- `high_entropy` - High Entropy generator name
|
||||||
|
- `xkcd_pass` - XKCD Pass generator name
|
||||||
|
- `diceware` - Diceware generator name
|
||||||
|
- `pwgen` - PWGen generator name
|
||||||
|
- `passkey` - Passkey generator name
|
||||||
|
|
||||||
|
#### **API Documentation**
|
||||||
|
- `api_documentation` - API Documentation link
|
||||||
|
- `download` - Download button text
|
||||||
|
- `interactive_api_explorer` - API Explorer heading
|
||||||
|
- `openapi_json` - OpenAPI JSON description
|
||||||
|
- `postman_collection` - Postman Collection description
|
||||||
|
|
||||||
|
## 🔧 How to Use These Tools
|
||||||
|
|
||||||
|
### **Step 1: Run the Extractor**
|
||||||
|
```bash
|
||||||
|
# Use the PHP extractor for comprehensive analysis
|
||||||
|
php extract-translations.php
|
||||||
|
|
||||||
|
# OR use the shell script for quick updates
|
||||||
|
./update-translations.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Step 2: Review Generated Files**
|
||||||
|
Check the `locales/` directory for:
|
||||||
|
- Updated JSON files for each language
|
||||||
|
- Any missing or placeholder translation entries that still need review
|
||||||
|
|
||||||
|
### **Step 3: Fix Hardcoded Strings**
|
||||||
|
|
||||||
|
**HTML Files:**
|
||||||
|
```html
|
||||||
|
<!-- BEFORE -->
|
||||||
|
<button>Generate</button>
|
||||||
|
<label>Language:</label>
|
||||||
|
|
||||||
|
<!-- AFTER -->
|
||||||
|
<button data-i18n="generate">Generate</button>
|
||||||
|
<label data-i18n="language">Language:</label>
|
||||||
|
```
|
||||||
|
|
||||||
|
**JavaScript Files:**
|
||||||
|
```javascript
|
||||||
|
// BEFORE
|
||||||
|
showNotification('Copied to clipboard!');
|
||||||
|
generateBtn.innerHTML = 'Generating...';
|
||||||
|
|
||||||
|
// AFTER
|
||||||
|
showNotification(this.t('copied_to_clipboard'));
|
||||||
|
generateBtn.innerHTML = this.t('generating');
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Step 4: Translate Placeholder Strings**
|
||||||
|
Replace placeholder entries in language files:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"generate": "Generate (TODO: de)", // ← Replace this
|
||||||
|
"copy": "Kopieren" // ✅ Already translated
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With proper translations:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"generate": "Generieren", // ✅ Properly translated
|
||||||
|
"copy": "Kopieren" // ✅ Already translated
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Translation Statistics
|
||||||
|
|
||||||
|
Run the tools to get current statistics:
|
||||||
|
|
||||||
|
```
|
||||||
|
📊 Translation Status Report
|
||||||
|
============================
|
||||||
|
en: 87/87 (100%) translated
|
||||||
|
de: 45/87 (52%) translated
|
||||||
|
fr: 32/87 (37%) translated
|
||||||
|
it: 15/87 (17%) translated
|
||||||
|
es: 15/87 (17%) translated
|
||||||
|
ja: 0/87 (0%) translated
|
||||||
|
zh: 0/87 (0%) translated
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Quick Win Translations
|
||||||
|
|
||||||
|
### **High Priority Strings (Visible to Users)**
|
||||||
|
1. `generate` → "Generate" button
|
||||||
|
2. `copy` → "Copy" button
|
||||||
|
3. `language` → Language selector
|
||||||
|
4. `loading_generators` → Loading message
|
||||||
|
5. `api_documentation` → Header link
|
||||||
|
6. `back_to_generator` → Navigation link
|
||||||
|
|
||||||
|
### **Common Translations Already Provided**
|
||||||
|
|
||||||
|
**German (de):**
|
||||||
|
- `generate` → "Generieren"
|
||||||
|
- `copy` → "Kopieren"
|
||||||
|
- `language` → "Sprache"
|
||||||
|
- `password_generator` → "Passwort-Generator"
|
||||||
|
|
||||||
|
**French (fr):**
|
||||||
|
- `generate` → "Générer"
|
||||||
|
- `copy` → "Copier"
|
||||||
|
- `language` → "Langue"
|
||||||
|
- `password_generator` → "Générateur de mots de passe"
|
||||||
|
|
||||||
|
## 🚨 Common Issues & Solutions
|
||||||
|
|
||||||
|
### **Issue: Missing data-i18n attributes**
|
||||||
|
**Solution:** Add `data-i18n="key"` to HTML elements
|
||||||
|
```html
|
||||||
|
<h1 data-i18n="app_title">Password Generator</h1>
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Issue: Hardcoded JavaScript strings**
|
||||||
|
**Solution:** Use `this.t()` method
|
||||||
|
```javascript
|
||||||
|
const message = this.t('copied_to_clipboard');
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Issue: Translation not appearing**
|
||||||
|
**Solution:** Check that:
|
||||||
|
1. Translation key exists in JSON file
|
||||||
|
2. `data-i18n` attribute is correct
|
||||||
|
3. `updateUI()` is called after content changes
|
||||||
|
|
||||||
|
## 📈 Improvement Roadmap
|
||||||
|
|
||||||
|
### **Phase 1: Core UI (Current)**
|
||||||
|
- ✅ Main navigation
|
||||||
|
- ✅ Generator cards
|
||||||
|
- ✅ Button text
|
||||||
|
- ✅ Form labels
|
||||||
|
|
||||||
|
### **Phase 2: API Documentation**
|
||||||
|
- 🔄 Swagger page content
|
||||||
|
- 🔄 Download descriptions
|
||||||
|
- 🔄 Example text
|
||||||
|
|
||||||
|
### **Phase 3: Advanced Features**
|
||||||
|
- ⏳ Error messages
|
||||||
|
- ⏳ Validation text
|
||||||
|
- ⏳ Help tooltips
|
||||||
|
|
||||||
|
## 💡 Best Practices
|
||||||
|
|
||||||
|
### **Translation Keys**
|
||||||
|
- Use `snake_case` format
|
||||||
|
- Be descriptive but concise
|
||||||
|
- Group related keys with prefixes:
|
||||||
|
- `error_*` for error messages
|
||||||
|
- `button_*` for button text
|
||||||
|
- `label_*` for form labels
|
||||||
|
|
||||||
|
### **String Extraction**
|
||||||
|
- Run tools regularly during development
|
||||||
|
- Check for new hardcoded strings after UI changes
|
||||||
|
- Update translations before releases
|
||||||
|
|
||||||
|
### **Quality Assurance**
|
||||||
|
- Test UI in multiple languages
|
||||||
|
- Check for text overflow in different languages
|
||||||
|
- Verify special characters display correctly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
|
|
||||||
|
1. **Run the extractor:**
|
||||||
|
```bash
|
||||||
|
php extract-translations.php
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Fix the highest-priority strings first**
|
||||||
|
|
||||||
|
3. **Test in multiple languages:**
|
||||||
|
```bash
|
||||||
|
# Change language in the UI dropdown
|
||||||
|
# Or visit: http://localhost:8080/?lang=de
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Iterate and improve**
|
||||||
|
|
||||||
|
Your translation management system is now ready to ensure a fully internationalized Password Generator application! 🌍✨
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
$output = [];
|
||||||
|
exec('which pwgen 2>/dev/null', $output, $returnVar);
|
||||||
|
|
||||||
|
var_dump($output, $returnVar);
|
||||||
|
echo "pwgen available: " . ($returnVar === 0 && !empty($output[0]) ? 'yes' : 'no') . "\n";
|
||||||
@@ -0,0 +1,723 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Translation Key Extraction Tool v3.1
|
||||||
|
*
|
||||||
|
* Comprehensive translation key extraction with multiple sources:
|
||||||
|
* 1. Generator API-driven keys (from generator definitions)
|
||||||
|
* 2. JavaScript files ($this.t() calls)
|
||||||
|
* 3. PHP files (static i18n keys)
|
||||||
|
* 4. Backup of existing translation keys
|
||||||
|
*
|
||||||
|
* Output format: JSON only
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* php bin/extract-translations.php [options]
|
||||||
|
*
|
||||||
|
* Options:
|
||||||
|
* --output=file Output file path
|
||||||
|
* --context Include context information
|
||||||
|
* --template Generate template with placeholders
|
||||||
|
* --language=en Language for template generation
|
||||||
|
* --backup Create backup of existing translations
|
||||||
|
* --merge Merge with existing translations
|
||||||
|
* --js-path=path Path to JavaScript files (default: public/assets/js)
|
||||||
|
* --php-path=path Path to PHP files (default: src)
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* php bin/extract-translations.php --output=locales/keys.json
|
||||||
|
* php bin/extract-translations.php --backup --merge --output=locales/en.json
|
||||||
|
* php bin/extract-translations.php --context --output=locales/keys-context.json
|
||||||
|
*
|
||||||
|
* @author Password Generator Project
|
||||||
|
* @version 3.1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use App\Managers\PasswordGeneratorManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main extraction class
|
||||||
|
*/
|
||||||
|
class TranslationKeyExtractor
|
||||||
|
{
|
||||||
|
private PasswordGeneratorManager $generatorManager;
|
||||||
|
private array $collectedKeys = [];
|
||||||
|
private array $keysWithContext = [];
|
||||||
|
private array $existingTranslations = [];
|
||||||
|
private array $stats = [
|
||||||
|
'generator_keys' => 0,
|
||||||
|
'js_keys' => 0,
|
||||||
|
'php_keys' => 0,
|
||||||
|
'existing_keys' => 0,
|
||||||
|
'new_keys' => 0,
|
||||||
|
'obsolete_keys' => 0
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(PasswordGeneratorManager $generatorManager)
|
||||||
|
{
|
||||||
|
$this->generatorManager = $generatorManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract all i18n keys from all sources
|
||||||
|
*/
|
||||||
|
public function extractAll(array $options = []): array
|
||||||
|
{
|
||||||
|
$jsPath = $options['js_path'] ?? 'public/assets/js';
|
||||||
|
$phpPath = $options['php_path'] ?? 'src';
|
||||||
|
$existingFile = $options['existing_file'] ?? null;
|
||||||
|
|
||||||
|
echo "=== Extraction Sources ===\n";
|
||||||
|
echo "JavaScript path: {$jsPath}\n";
|
||||||
|
echo "PHP path: {$phpPath}\n";
|
||||||
|
|
||||||
|
if ($existingFile && file_exists($existingFile)) {
|
||||||
|
echo "Existing translations: {$existingFile}\n";
|
||||||
|
$this->loadExistingTranslations($existingFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Extract from generators (API-driven)
|
||||||
|
echo "=== Extracting from Password Generators ===\n";
|
||||||
|
$this->extractFromGenerators();
|
||||||
|
|
||||||
|
// Extract from JavaScript files
|
||||||
|
echo "\n=== Extracting from JavaScript Files ===\n";
|
||||||
|
$this->extractFromJavaScriptFiles($jsPath);
|
||||||
|
|
||||||
|
// Extract from PHP files
|
||||||
|
echo "\n=== Extracting from PHP Files ===\n";
|
||||||
|
$this->extractFromPhpFiles($phpPath);
|
||||||
|
|
||||||
|
// Compare with existing
|
||||||
|
if (!empty($this->existingTranslations)) {
|
||||||
|
echo "\n=== Comparing with Existing Translations ===\n";
|
||||||
|
$this->compareWithExisting();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort keys alphabetically
|
||||||
|
ksort($this->collectedKeys);
|
||||||
|
ksort($this->keysWithContext);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'keys' => $this->collectedKeys,
|
||||||
|
'keysWithContext' => $this->keysWithContext,
|
||||||
|
'stats' => $this->getStatistics(),
|
||||||
|
'existingTranslations' => $this->existingTranslations
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load existing translations for comparison/merging
|
||||||
|
*/
|
||||||
|
private function loadExistingTranslations(string $file): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (!str_ends_with($file, '.json')) {
|
||||||
|
throw new Exception('Only JSON format is supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
$this->existingTranslations = json_decode($content, true) ?? [];
|
||||||
|
|
||||||
|
// Remove comment keys
|
||||||
|
$this->existingTranslations = array_filter(
|
||||||
|
$this->existingTranslations,
|
||||||
|
fn($key) => !str_starts_with($key, '_comment'),
|
||||||
|
ARRAY_FILTER_USE_KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->stats['existing_keys'] = count($this->existingTranslations);
|
||||||
|
echo "✅ Loaded {$this->stats['existing_keys']} existing translations\n";
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "⚠️ Failed to load existing translations: {$e->getMessage()}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract keys from password generators using API-driven approach
|
||||||
|
*/
|
||||||
|
private function extractFromGenerators(): void
|
||||||
|
{
|
||||||
|
$generators = $this->generatorManager->getAvailableTypes();
|
||||||
|
|
||||||
|
foreach ($generators as $generator) {
|
||||||
|
$this->extractFromGenerator($generator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract keys from a single generator
|
||||||
|
*/
|
||||||
|
private function extractFromGenerator($generator): void
|
||||||
|
{
|
||||||
|
$generatorId = $generator->getId();
|
||||||
|
echo "📦 {$generatorId}...\n";
|
||||||
|
|
||||||
|
// Get all keys using the generator's built-in method
|
||||||
|
$keys = $generator->getAllI18nKeys();
|
||||||
|
|
||||||
|
// Add keys with context information
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
$this->addKey($key, 'generator', $generatorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stats['generator_keys'] += count($keys);
|
||||||
|
echo " → Found " . count($keys) . " keys\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract i18n keys from JavaScript files
|
||||||
|
* Looks for: this.t('key'), $this.t('key'), app.t('key')
|
||||||
|
*/
|
||||||
|
private function extractFromJavaScriptFiles(string $basePath): void
|
||||||
|
{
|
||||||
|
if (!is_dir($basePath)) {
|
||||||
|
echo "⚠️ JavaScript path not found: {$basePath}\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = $this->findFiles($basePath, ['js']);
|
||||||
|
echo "Found " . count($files) . " JavaScript files\n";
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$this->extractFromJavaScriptFile($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract keys from a single JavaScript file
|
||||||
|
*/
|
||||||
|
private function extractFromJavaScriptFile(string $file): void
|
||||||
|
{
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
$filename = basename($file);
|
||||||
|
|
||||||
|
// Pattern for this.t('key'), $this.t('key'), this.t("key")
|
||||||
|
$patterns = [
|
||||||
|
'/(?:this|\\$this)\\.t\\([\'"]([^\'"]+)[\'"]\\)/',
|
||||||
|
'/app\\.t\\([\'"]([^\'"]+)[\'"]\\)/',
|
||||||
|
'/t\\([\'"]([^\'"]+)[\'"]\\)/', // Simple t() calls
|
||||||
|
];
|
||||||
|
|
||||||
|
$foundKeys = [];
|
||||||
|
foreach ($patterns as $pattern) {
|
||||||
|
if (preg_match_all($pattern, $content, $matches)) {
|
||||||
|
$foundKeys = array_merge($foundKeys, $matches[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$foundKeys = array_unique($foundKeys);
|
||||||
|
|
||||||
|
if (!empty($foundKeys)) {
|
||||||
|
echo " 📄 {$filename}: " . count($foundKeys) . " keys\n";
|
||||||
|
|
||||||
|
foreach ($foundKeys as $key) {
|
||||||
|
$this->addKey($key, 'javascript', $filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stats['js_keys'] += count($foundKeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract i18n keys from PHP files
|
||||||
|
* Looks for static string keys in specific patterns
|
||||||
|
*/
|
||||||
|
private function extractFromPhpFiles(string $basePath): void
|
||||||
|
{
|
||||||
|
if (!is_dir($basePath)) {
|
||||||
|
echo "⚠️ PHP path not found: {$basePath}\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = $this->findFiles($basePath, ['php']);
|
||||||
|
echo "Found " . count($files) . " PHP files\n";
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$this->extractFromPhpFile($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract keys from a single PHP file
|
||||||
|
* Looks for static keys in comments or constants
|
||||||
|
*/
|
||||||
|
private function extractFromPhpFile(string $file): void
|
||||||
|
{
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
$filename = basename($file);
|
||||||
|
|
||||||
|
// Pattern for static keys in comments like: // i18n: key_name
|
||||||
|
$patterns = [
|
||||||
|
'/\\/\\/\\s*i18n:\\s*([a-z0-9_]+)/i',
|
||||||
|
'/\\/\\*\\*.*?@i18n\\s+([a-z0-9_]+).*?\\*\\//s',
|
||||||
|
// Add more patterns as needed
|
||||||
|
];
|
||||||
|
|
||||||
|
$foundKeys = [];
|
||||||
|
foreach ($patterns as $pattern) {
|
||||||
|
if (preg_match_all($pattern, $content, $matches)) {
|
||||||
|
$foundKeys = array_merge($foundKeys, $matches[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$foundKeys = array_unique($foundKeys);
|
||||||
|
|
||||||
|
if (!empty($foundKeys)) {
|
||||||
|
echo " 📄 {$filename}: " . count($foundKeys) . " keys\n";
|
||||||
|
|
||||||
|
foreach ($foundKeys as $key) {
|
||||||
|
$this->addKey($key, 'php', $filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stats['php_keys'] += count($foundKeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find files with specific extensions recursively
|
||||||
|
*/
|
||||||
|
private function findFiles(string $dir, array $extensions): array
|
||||||
|
{
|
||||||
|
$files = [];
|
||||||
|
$iterator = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($iterator as $file) {
|
||||||
|
if ($file->isFile()) {
|
||||||
|
$ext = strtolower($file->getExtension());
|
||||||
|
if (in_array($ext, $extensions)) {
|
||||||
|
$files[] = $file->getPathname();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a key to the collection with context
|
||||||
|
*/
|
||||||
|
private function addKey(string $key, string $source, string $sourceDetail = ''): void
|
||||||
|
{
|
||||||
|
// Initialize with existing translation if available
|
||||||
|
if (!isset($this->collectedKeys[$key])) {
|
||||||
|
$this->collectedKeys[$key] = $this->existingTranslations[$key] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add or update context
|
||||||
|
if (!isset($this->keysWithContext[$key])) {
|
||||||
|
$this->keysWithContext[$key] = [
|
||||||
|
'key' => $key,
|
||||||
|
'sources' => [],
|
||||||
|
'type' => $this->getKeyType($key),
|
||||||
|
'description' => $this->getKeyDescription($key)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add source information
|
||||||
|
$this->keysWithContext[$key]['sources'][] = [
|
||||||
|
'type' => $source,
|
||||||
|
'detail' => $sourceDetail
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get key type from key pattern
|
||||||
|
*/
|
||||||
|
private function getKeyType(string $key): string
|
||||||
|
{
|
||||||
|
if (str_ends_with($key, '_name')) {
|
||||||
|
return 'generator_metadata';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_ends_with($key, '_description')) {
|
||||||
|
return 'generator_metadata';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($key, '_option_') && str_ends_with($key, '_label')) {
|
||||||
|
return 'option_label';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($key, '_option_') && str_ends_with($key, '_description')) {
|
||||||
|
return 'option_description';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($key, '_option_') && str_contains($key, '_choice_')) {
|
||||||
|
return 'option_choice_label';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_ends_with($key, '_validation_error')) {
|
||||||
|
return 'validation_error';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($key, '_options_group_') && str_ends_with($key, '_title')) {
|
||||||
|
return 'group_title';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'general';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get key description
|
||||||
|
*/
|
||||||
|
private function getKeyDescription(string $key): string
|
||||||
|
{
|
||||||
|
$type = $this->getKeyType($key);
|
||||||
|
|
||||||
|
switch ($type) {
|
||||||
|
case 'generator_metadata':
|
||||||
|
return str_ends_with($key, '_name') ? 'Generator display name' : 'Generator description';
|
||||||
|
|
||||||
|
case 'option_label':
|
||||||
|
preg_match('/_option_(.+)_label$/', $key, $matches);
|
||||||
|
return 'Label for option: ' . ($matches[1] ?? 'unknown');
|
||||||
|
|
||||||
|
case 'option_description':
|
||||||
|
preg_match('/_option_(.+)_description$/', $key, $matches);
|
||||||
|
return 'Description for option: ' . ($matches[1] ?? 'unknown');
|
||||||
|
|
||||||
|
case 'option_choice_label':
|
||||||
|
preg_match('/_option_(.+)_choice_(.+)$/', $key, $matches);
|
||||||
|
return 'Choice label for: ' . ($matches[1] ?? 'unknown') . ' = ' . ($matches[2] ?? 'unknown');
|
||||||
|
|
||||||
|
case 'validation_error':
|
||||||
|
preg_match('/_option_(.+)_validation_error$/', $key, $matches);
|
||||||
|
return 'Validation error for: ' . ($matches[1] ?? 'unknown');
|
||||||
|
|
||||||
|
case 'group_title':
|
||||||
|
preg_match('/_options_group_(.+)_title$/', $key, $matches);
|
||||||
|
return 'Group title for: ' . ($matches[1] ?? 'unknown');
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 'General UI text';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare extracted keys with existing translations
|
||||||
|
*/
|
||||||
|
private function compareWithExisting(): void
|
||||||
|
{
|
||||||
|
$extractedKeys = array_keys($this->collectedKeys);
|
||||||
|
$existingKeys = array_keys($this->existingTranslations);
|
||||||
|
|
||||||
|
$newKeys = array_diff($extractedKeys, $existingKeys);
|
||||||
|
$obsoleteKeys = array_diff($existingKeys, $extractedKeys);
|
||||||
|
|
||||||
|
$this->stats['new_keys'] = count($newKeys);
|
||||||
|
$this->stats['obsolete_keys'] = count($obsoleteKeys);
|
||||||
|
|
||||||
|
if (!empty($newKeys)) {
|
||||||
|
echo "🆕 New keys: " . count($newKeys) . "\n";
|
||||||
|
foreach (array_slice($newKeys, 0, 10) as $key) {
|
||||||
|
echo " + {$key}\n";
|
||||||
|
}
|
||||||
|
if (count($newKeys) > 10) {
|
||||||
|
echo " ... and " . (count($newKeys) - 10) . " more\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($obsoleteKeys)) {
|
||||||
|
echo "\n⚠️ Obsolete keys (not found in code): " . count($obsoleteKeys) . "\n";
|
||||||
|
foreach (array_slice($obsoleteKeys, 0, 10) as $key) {
|
||||||
|
echo " - {$key}\n";
|
||||||
|
}
|
||||||
|
if (count($obsoleteKeys) > 10) {
|
||||||
|
echo " ... and " . (count($obsoleteKeys) - 10) . " more\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get extraction statistics
|
||||||
|
*/
|
||||||
|
private function getStatistics(): array
|
||||||
|
{
|
||||||
|
$byType = [];
|
||||||
|
$bySource = [];
|
||||||
|
|
||||||
|
foreach ($this->keysWithContext as $keyData) {
|
||||||
|
$type = $keyData['type'];
|
||||||
|
$byType[$type] = ($byType[$type] ?? 0) + 1;
|
||||||
|
|
||||||
|
foreach ($keyData['sources'] as $source) {
|
||||||
|
$sourceType = $source['type'];
|
||||||
|
$bySource[$sourceType] = ($bySource[$sourceType] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_merge($this->stats, [
|
||||||
|
'total_keys' => count($this->collectedKeys),
|
||||||
|
'by_type' => $byType,
|
||||||
|
'by_source' => $bySource
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create backup of existing translations
|
||||||
|
*/
|
||||||
|
public function createBackup(string $file): bool
|
||||||
|
{
|
||||||
|
if (!file_exists($file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$backupFile = $file . '.backup.' . date('Y-m-d_His');
|
||||||
|
|
||||||
|
if (copy($file, $backupFile)) {
|
||||||
|
echo "💾 Created backup: {$backupFile}\n";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export keys to JSON format
|
||||||
|
*/
|
||||||
|
public function exportToJson(): string
|
||||||
|
{
|
||||||
|
return json_encode($this->collectedKeys, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export keys with context to JSON format
|
||||||
|
*/
|
||||||
|
public function exportToJsonWithContext(): string
|
||||||
|
{
|
||||||
|
return json_encode([
|
||||||
|
'keys' => $this->collectedKeys,
|
||||||
|
'context' => array_values($this->keysWithContext),
|
||||||
|
'stats' => $this->getStatistics()
|
||||||
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate template translation file with placeholders
|
||||||
|
*/
|
||||||
|
public function generateTemplate(string $language = 'en'): string
|
||||||
|
{
|
||||||
|
$template = [];
|
||||||
|
|
||||||
|
$lastGenerator = '';
|
||||||
|
foreach ($this->keysWithContext as $keyData) {
|
||||||
|
$key = $keyData['key'];
|
||||||
|
|
||||||
|
// Add section comments for generators
|
||||||
|
if (str_contains($key, '_') && !str_starts_with($key, '_comment')) {
|
||||||
|
$generatorId = explode('_', $key)[0];
|
||||||
|
if ($generatorId !== $lastGenerator) {
|
||||||
|
$template["_comment_{$generatorId}"] = strtoupper($generatorId) . " Generator";
|
||||||
|
$lastGenerator = $generatorId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use existing translation or generate placeholder
|
||||||
|
$value = $this->existingTranslations[$key] ?? $this->generatePlaceholder($keyData);
|
||||||
|
$template[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_encode($template, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate placeholder text for a key
|
||||||
|
*/
|
||||||
|
private function generatePlaceholder(array $keyData): string
|
||||||
|
{
|
||||||
|
$type = $keyData['type'];
|
||||||
|
$key = $keyData['key'];
|
||||||
|
|
||||||
|
// Extract meaningful part from key
|
||||||
|
if (preg_match('/_option_([^_]+)_/', $key, $matches)) {
|
||||||
|
$field = str_replace('_', ' ', $matches[1]);
|
||||||
|
$field = ucwords($field);
|
||||||
|
} else {
|
||||||
|
$field = 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($type) {
|
||||||
|
case 'generator_metadata':
|
||||||
|
return str_ends_with($key, '_name')
|
||||||
|
? 'Generator Name'
|
||||||
|
: 'Generator description text';
|
||||||
|
|
||||||
|
case 'option_label':
|
||||||
|
return $field;
|
||||||
|
|
||||||
|
case 'option_description':
|
||||||
|
return "Description for {$field}";
|
||||||
|
|
||||||
|
case 'option_choice_label':
|
||||||
|
if (preg_match('/_choice_([^_]+)$/', $key, $matches)) {
|
||||||
|
return ucfirst(str_replace('_', ' ', $matches[1]));
|
||||||
|
}
|
||||||
|
return ucfirst($field);
|
||||||
|
|
||||||
|
case 'validation_error':
|
||||||
|
return "Invalid value for {$field}";
|
||||||
|
|
||||||
|
case 'group_title':
|
||||||
|
return $field . " Options";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLI Interface
|
||||||
|
*/
|
||||||
|
class CLI
|
||||||
|
{
|
||||||
|
private array $options = [];
|
||||||
|
|
||||||
|
public function __construct(array $argv)
|
||||||
|
{
|
||||||
|
$this->parseArguments($argv);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseArguments(array $argv): void
|
||||||
|
{
|
||||||
|
for ($i = 1; $i < count($argv); $i++) {
|
||||||
|
if (str_starts_with($argv[$i], '--')) {
|
||||||
|
$parts = explode('=', substr($argv[$i], 2), 2);
|
||||||
|
$this->options[$parts[0]] = $parts[1] ?? true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOption(string $name, $default = null)
|
||||||
|
{
|
||||||
|
return $this->options[$name] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasOption(string $name): bool
|
||||||
|
{
|
||||||
|
return isset($this->options[$name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main execution
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
echo "=============================================\n";
|
||||||
|
echo "Translation Key Extraction Tool v3.1\n";
|
||||||
|
echo "JSON Format Only\n";
|
||||||
|
echo "=============================================\n\n";
|
||||||
|
|
||||||
|
$cli = new CLI($argv);
|
||||||
|
|
||||||
|
// Get options
|
||||||
|
$output = $cli->getOption('output');
|
||||||
|
$withContext = $cli->hasOption('context');
|
||||||
|
$template = $cli->hasOption('template');
|
||||||
|
$language = $cli->getOption('language', 'en');
|
||||||
|
$backup = $cli->hasOption('backup');
|
||||||
|
$merge = $cli->hasOption('merge');
|
||||||
|
$jsPath = $cli->getOption('js-path', 'public/assets/js');
|
||||||
|
$phpPath = $cli->getOption('php-path', 'src');
|
||||||
|
|
||||||
|
// Initialize generator manager
|
||||||
|
$generatorManager = new PasswordGeneratorManager();
|
||||||
|
|
||||||
|
// Extract keys
|
||||||
|
$extractor = new TranslationKeyExtractor($generatorManager);
|
||||||
|
|
||||||
|
$extractOptions = [
|
||||||
|
'js_path' => $jsPath,
|
||||||
|
'php_path' => $phpPath
|
||||||
|
];
|
||||||
|
|
||||||
|
// Load existing translations if merging or backing up
|
||||||
|
if (($merge || $backup) && $output && file_exists($output)) {
|
||||||
|
$extractOptions['existing_file'] = $output;
|
||||||
|
|
||||||
|
// Create backup if requested
|
||||||
|
if ($backup) {
|
||||||
|
$extractor->createBackup($output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $extractor->extractAll($extractOptions);
|
||||||
|
|
||||||
|
// Display statistics
|
||||||
|
echo "\n=== Extraction Statistics ===\n";
|
||||||
|
echo "Total keys: {$result['stats']['total_keys']}\n";
|
||||||
|
echo "Generator keys: {$result['stats']['generator_keys']}\n";
|
||||||
|
echo "JavaScript keys: {$result['stats']['js_keys']}\n";
|
||||||
|
echo "PHP keys: {$result['stats']['php_keys']}\n";
|
||||||
|
|
||||||
|
if ($result['stats']['existing_keys'] > 0) {
|
||||||
|
echo "Existing keys: {$result['stats']['existing_keys']}\n";
|
||||||
|
echo "New keys: {$result['stats']['new_keys']}\n";
|
||||||
|
echo "Obsolete keys: {$result['stats']['obsolete_keys']}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nBy type:\n";
|
||||||
|
foreach ($result['stats']['by_type'] as $type => $count) {
|
||||||
|
echo " - {$type}: {$count}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nBy source:\n";
|
||||||
|
foreach ($result['stats']['by_source'] as $source => $count) {
|
||||||
|
echo " - {$source}: {$count}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate output
|
||||||
|
$outputContent = '';
|
||||||
|
|
||||||
|
if ($template) {
|
||||||
|
echo "\n=== Generating template file ===\n";
|
||||||
|
$outputContent = $extractor->generateTemplate($language);
|
||||||
|
} else {
|
||||||
|
echo "\n=== Generating JSON output ===\n";
|
||||||
|
$outputContent = $withContext
|
||||||
|
? $extractor->exportToJsonWithContext()
|
||||||
|
: $extractor->exportToJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save or display
|
||||||
|
if ($output) {
|
||||||
|
file_put_contents($output, $outputContent);
|
||||||
|
echo "✅ Saved to: {$output}\n";
|
||||||
|
} else {
|
||||||
|
echo "\n=== Output ===\n";
|
||||||
|
echo $outputContent;
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n✅ Extraction complete!\n";
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "ERROR: {$e->getMessage()}\n";
|
||||||
|
echo $e->getTraceAsString() . "\n";
|
||||||
|
echo "\nUsage:\n";
|
||||||
|
echo " php bin/extract-translations.php [options]\n\n";
|
||||||
|
echo "Options:\n";
|
||||||
|
echo " --output=file Output file path (JSON only)\n";
|
||||||
|
echo " --context Include context information\n";
|
||||||
|
echo " --template Generate template with placeholders\n";
|
||||||
|
echo " --language=en Language for template generation\n";
|
||||||
|
echo " --backup Create backup of existing translations\n";
|
||||||
|
echo " --merge Merge with existing translations\n";
|
||||||
|
echo " --js-path=path Path to JavaScript files (default: public/assets/js)\n";
|
||||||
|
echo " --php-path=path Path to PHP files (default: src)\n\n";
|
||||||
|
echo "Examples:\n";
|
||||||
|
echo " php bin/extract-translations.php --output=locales/keys.json\n";
|
||||||
|
echo " php bin/extract-translations.php --backup --merge --output=locales/en.json\n";
|
||||||
|
echo " php bin/extract-translations.php --context --output=locales/keys-context.json\n";
|
||||||
|
echo " php bin/extract-translations.php --template --output=locales/en-template.json\n";
|
||||||
|
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use App\Api\OpenApiGenerator;
|
||||||
|
|
||||||
|
$baseUrl = $_ENV['APP_PUBLIC_BASE_URL'] ?? $_ENV['APP_URL'] ?? 'http://localhost:8080';
|
||||||
|
if (isset($argv[1])) {
|
||||||
|
$baseUrl = $argv[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
$destination = __DIR__ . '/../public/pwgenerator-swagger-spec.json';
|
||||||
|
OpenApiGenerator::write($baseUrl, $destination);
|
||||||
|
|
||||||
|
echo "OpenAPI spec generated at {$destination}\n";
|
||||||
|
echo "Base URL: {$baseUrl}\n";
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
Used to collect words and their definitions from the Swiss German Slängikon page.
|
||||||
|
https://zuri.net/de/zurich/slängikon.htm
|
||||||
|
*/
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
var output = [];
|
||||||
|
$('h2').each(function() {
|
||||||
|
var heading = $(this).text().trim();
|
||||||
|
var headingElem = this; // Native DOM element
|
||||||
|
var nextSibling = headingElem.nextSibling;
|
||||||
|
var listText = '';
|
||||||
|
while (nextSibling) {
|
||||||
|
if (nextSibling.nodeType === 3) { // Text node
|
||||||
|
var text = nextSibling.textContent.trim();
|
||||||
|
if (text) {
|
||||||
|
listText += text + ' ';
|
||||||
|
}
|
||||||
|
} else if (nextSibling.nodeType === 1 && (nextSibling.tagName === 'HR' || nextSibling.tagName === 'H2')) {
|
||||||
|
break; // Stop at <hr> or next <h2>
|
||||||
|
}
|
||||||
|
nextSibling = nextSibling.nextSibling;
|
||||||
|
}
|
||||||
|
// Process the collected listText
|
||||||
|
if (listText) {
|
||||||
|
var words = listText.split(/ ?\| ?/); // Split by ' | ' or '|'
|
||||||
|
words.forEach(function(word) {
|
||||||
|
word = word.trim();
|
||||||
|
if (!word) return;
|
||||||
|
// Extract content in parentheses at the end
|
||||||
|
var explanation = '';
|
||||||
|
var match = word.match(/\s*\(([^)]+)\)$/);
|
||||||
|
if (match) {
|
||||||
|
explanation = match[1].trim();
|
||||||
|
word = word.replace(/\s*\([^)]+\)$/, '').trim();
|
||||||
|
}
|
||||||
|
// Remove leading articles 'e ', 'es ', 'en '
|
||||||
|
word = word.replace(/^(e |es |en |d |de |s )\b/i, '');
|
||||||
|
// Construct definition: heading + (explanation if present)
|
||||||
|
var definition = heading;
|
||||||
|
if (explanation) {
|
||||||
|
definition += ' (' + explanation + ')';
|
||||||
|
}
|
||||||
|
// Add to output
|
||||||
|
output.push(word + '\t' + definition);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Output the list to console, one word per line
|
||||||
|
if (output.length > 0) {
|
||||||
|
var outputText = output.join('\n');
|
||||||
|
console.log(outputText);
|
||||||
|
// Copy to clipboard
|
||||||
|
navigator.clipboard.writeText(outputText).then(function() {
|
||||||
|
console.log('Output successfully copied to clipboard.');
|
||||||
|
}).catch(function(err) {
|
||||||
|
console.error('Failed to copy to clipboard: ', err);
|
||||||
|
console.log('You can manually copy the output from the console above.');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('No words found. Please ensure jQuery is loaded and verify the page structure.');
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Translation Update Script
|
||||||
|
# Automatically extracts and updates translation strings
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🌍 Translation Update Utility"
|
||||||
|
echo "============================="
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
LOCALES_DIR="../locales"
|
||||||
|
TEMPLATES_DIR="../templates"
|
||||||
|
ASSETS_DIR="../assets"
|
||||||
|
SRC_DIR="../src"
|
||||||
|
|
||||||
|
# Create locales directory if it doesn't exist
|
||||||
|
mkdir -p "$LOCALES_DIR"
|
||||||
|
|
||||||
|
echo -e "${BLUE}📁 Scanning for untranslated strings...${NC}"
|
||||||
|
|
||||||
|
# Find all HTML files and extract data-i18n attributes
|
||||||
|
echo -e "${YELLOW}🔍 Scanning HTML templates...${NC}"
|
||||||
|
find $TEMPLATES_DIR -name "*.html" -exec grep -oE 'data-i18n="[^"]*"' {} \; | \
|
||||||
|
sed 's/data-i18n="//g' | sed 's/"//g' | sort | uniq > /tmp/html_keys.txt
|
||||||
|
|
||||||
|
find $TEMPLATES_DIR -name "*.html" -exec grep -oE 'data-i18n-placeholder="[^"]*"' {} \; | \
|
||||||
|
sed 's/data-i18n-placeholder="//g' | sed 's/"//g' | sort | uniq >> /tmp/html_keys.txt
|
||||||
|
|
||||||
|
find $TEMPLATES_DIR -name "*.html" -exec grep -oE 'data-i18n-title="[^"]*"' {} \; | \
|
||||||
|
sed 's/data-i18n-title="//g' | sed 's/"//g' | sort | uniq >> /tmp/html_keys.txt
|
||||||
|
|
||||||
|
# Find all PHP files and extract data-i18n attributes
|
||||||
|
echo -e "${YELLOW}🔍 Scanning PHP templates...${NC}"
|
||||||
|
find $SRC_DIR -name "*.php" -exec grep -oE 'data-i18n="[^"]*"' {} \; | \
|
||||||
|
sed 's/data-i18n="//g' | sed 's/"//g' | sort | uniq > /tmp/php_keys.txt
|
||||||
|
|
||||||
|
find $SRC_DIR -name "*.php" -exec grep -oE 'data-i18n-placeholder="[^"]*"' {} \; | \
|
||||||
|
sed 's/data-i18n-placeholder="//g' | sed 's/"//g' | sort | uniq >> /tmp/php_keys.txt
|
||||||
|
|
||||||
|
find $SRC_DIR -name "*.php" -exec grep -oE 'data-i18n-title="[^"]*"' {} \; | \
|
||||||
|
sed 's/data-i18n-title="//g' | sed 's/"//g' | sort | uniq >> /tmp/php_keys.txt
|
||||||
|
|
||||||
|
# Find JavaScript files and extract this.t() calls
|
||||||
|
echo -e "${YELLOW}🔍 Scanning JavaScript files...${NC}"
|
||||||
|
find $ASSETS_DIR -name "*.js" -exec grep -oE "this\.t\('[^']*'\)" {} \; | \
|
||||||
|
sed "s/this\.t('//g" | sed "s/')//g" | sort | uniq > /tmp/js_keys.txt
|
||||||
|
|
||||||
|
find $ASSETS_DIR -name "*.js" -exec grep -oE 'this\.t\("[^"]*"\)' {} \; | \
|
||||||
|
sed 's/this\.t("//g' | sed 's/")//g' | sort | uniq >> /tmp/js_keys.txt
|
||||||
|
|
||||||
|
# Combine all keys
|
||||||
|
cat /tmp/html_keys.txt /tmp/js_keys.txt /tmp/php_keys.txt | sort | uniq > /tmp/all_keys.txt
|
||||||
|
|
||||||
|
echo -e "${BLUE}📊 Found $(cat /tmp/all_keys.txt | wc -l) unique translation keys${NC}"
|
||||||
|
|
||||||
|
# Function to create/update language file
|
||||||
|
update_language_file() {
|
||||||
|
local lang=$1
|
||||||
|
local file="$LOCALES_DIR/$lang.json"
|
||||||
|
|
||||||
|
echo -e "${YELLOW}📝 Updating $lang.json...${NC}"
|
||||||
|
|
||||||
|
# Load existing translations or create empty object
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
cp "$file" "/tmp/${lang}_backup.json"
|
||||||
|
existing_keys=$(cat "$file" | jq -r 'keys[]' 2>/dev/null || echo "")
|
||||||
|
else
|
||||||
|
echo "{}" > "$file"
|
||||||
|
existing_keys=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create temporary file with all required keys
|
||||||
|
echo "{" > "/tmp/${lang}_new.json"
|
||||||
|
first=true
|
||||||
|
|
||||||
|
while IFS= read -r key; do
|
||||||
|
if [ ! -z "$key" ]; then
|
||||||
|
# Check if key already exists
|
||||||
|
existing_value=$(cat "$file" | jq -r ".[\"$key\"] // empty" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ ! -z "$existing_value" ] && [ "$existing_value" != "null" ]; then
|
||||||
|
# Keep existing translation
|
||||||
|
value="$existing_value"
|
||||||
|
else
|
||||||
|
# Add new key with placeholder or translation
|
||||||
|
case $lang in
|
||||||
|
"en")
|
||||||
|
# Convert snake_case to human readable
|
||||||
|
value=$(echo "$key" | sed 's/_/ /g' | sed 's/\b\(.\)/\u\1/g')
|
||||||
|
;;
|
||||||
|
"de")
|
||||||
|
case $key in
|
||||||
|
"generate") value="Generieren" ;;
|
||||||
|
"copy") value="Kopieren" ;;
|
||||||
|
"language") value="Sprache" ;;
|
||||||
|
"options") value="Optionen" ;;
|
||||||
|
"password_generator") value="Passwort-Generator" ;;
|
||||||
|
"api_documentation") value="API-Dokumentation" ;;
|
||||||
|
"loading") value="Wird geladen..." ;;
|
||||||
|
"error") value="Fehler" ;;
|
||||||
|
"success") value="Erfolgreich" ;;
|
||||||
|
"download") value="Herunterladen" ;;
|
||||||
|
"back") value="Zurück" ;;
|
||||||
|
"home") value="Startseite" ;;
|
||||||
|
"settings") value="Einstellungen" ;;
|
||||||
|
*) value="$(echo "$key" | sed 's/_/ /g' | sed 's/\b\(.\)/\u\1/g') (DE)" ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
"fr")
|
||||||
|
case $key in
|
||||||
|
"generate") value="Générer" ;;
|
||||||
|
"copy") value="Copier" ;;
|
||||||
|
"language") value="Langue" ;;
|
||||||
|
"options") value="Options" ;;
|
||||||
|
"password_generator") value="Générateur de mots de passe" ;;
|
||||||
|
"api_documentation") value="Documentation API" ;;
|
||||||
|
"loading") value="Chargement..." ;;
|
||||||
|
"error") value="Erreur" ;;
|
||||||
|
"success") value="Succès" ;;
|
||||||
|
"download") value="Télécharger" ;;
|
||||||
|
"back") value="Retour" ;;
|
||||||
|
"home") value="Accueil" ;;
|
||||||
|
"settings") value="Paramètres" ;;
|
||||||
|
*) value="$(echo "$key" | sed 's/_/ /g' | sed 's/\b\(.\)/\u\1/g') (FR)" ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
value="$(echo "$key" | sed 's/_/ /g' | sed 's/\b\(.\)/\u\1/g') (TODO: $lang)"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add to JSON
|
||||||
|
if [ "$first" = true ]; then
|
||||||
|
first=false
|
||||||
|
else
|
||||||
|
echo "," >> "/tmp/${lang}_new.json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape quotes in value
|
||||||
|
escaped_value=$(echo "$value" | sed 's/"/\\"/g')
|
||||||
|
echo " \"$key\": \"$escaped_value\"" >> "/tmp/${lang}_new.json"
|
||||||
|
fi
|
||||||
|
done < /tmp/all_keys.txt
|
||||||
|
|
||||||
|
echo "" >> "/tmp/${lang}_new.json"
|
||||||
|
echo "}" >> "/tmp/${lang}_new.json"
|
||||||
|
|
||||||
|
# Format JSON nicely
|
||||||
|
cat "/tmp/${lang}_new.json" | jq '.' > "$file"
|
||||||
|
|
||||||
|
# Count translations
|
||||||
|
total_keys=$(cat /tmp/all_keys.txt | wc -l)
|
||||||
|
translated_keys=$(cat "$file" | jq -r 'to_entries[] | select(.value | test("\\(TODO:|\\(DE\\)|\\(FR\\)|\\(IT\\)|\\(ES\\)|\\(JA\\)|\\(ZH\\))") | not) | .key' | wc -l)
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ Updated $lang.json: $translated_keys/$total_keys translated${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update all language files
|
||||||
|
languages=("en" "de" "fr" "it" "es" "ja" "zh")
|
||||||
|
|
||||||
|
for lang in "${languages[@]}"; do
|
||||||
|
update_language_file "$lang"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Generate report
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}📊 Translation Status Report${NC}"
|
||||||
|
echo "============================"
|
||||||
|
|
||||||
|
for lang in "${languages[@]}"; do
|
||||||
|
file="$LOCALES_DIR/$lang.json"
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
total=$(cat /tmp/all_keys.txt | wc -l)
|
||||||
|
translated=$(cat "$file" | jq -r 'to_entries[] | select(.value | test("\\(TODO:|\\(DE\\)|\\(FR\\)|\\(IT\\)|\\(ES\\)|\\(JA\\)|\\(ZH\\))") | not) | .key' | wc -l 2>/dev/null || echo "0")
|
||||||
|
percentage=$((translated * 100 / total))
|
||||||
|
|
||||||
|
echo -e "$lang: $translated/$total ($percentage%) translated"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Find hardcoded strings that need data-i18n attributes
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}🔍 Finding hardcoded strings that need localization...${NC}"
|
||||||
|
|
||||||
|
# Check HTML files for hardcoded text
|
||||||
|
echo -e "${BLUE}HTML files with potential hardcoded text:${NC}"
|
||||||
|
find $TEMPLATES_DIR -name "*.html" -exec grep -l '[>][A-Z][^<]*[a-z][^<]*[<]' {} \; | while read file; do
|
||||||
|
echo " 📄 $file"
|
||||||
|
grep -oE '>[A-Z][^<]*[a-z][^<]*<' "$file" | sed 's/^>//; s/<$//' | head -5 | while read text; do
|
||||||
|
if [[ ! "$text" =~ ^(GET|POST|PUT|DELETE|HTTP|JSON|API|URL|ID|OK|CSS|JS|HTML)$ ]]; then
|
||||||
|
echo " → \"$text\" (needs data-i18n)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check JavaScript files for hardcoded strings
|
||||||
|
echo -e "${BLUE}JavaScript files with potential hardcoded text:${NC}"
|
||||||
|
find $ASSETS_DIR -name "*.js" -exec grep -l "'[A-Z][^']*[a-z][^']*'" {} \; | while read file; do
|
||||||
|
echo " 📄 $file"
|
||||||
|
grep -oE "'[A-Z][^']*[a-z][^']*'" "$file" | sed "s/^'//; s/'$//" | head -5 | while read text; do
|
||||||
|
if [[ ! "$text" =~ ^(GET|POST|PUT|DELETE|HTTP|JSON|API|URL|ID|OK|CSS|JS|HTML|Content-Type|application/json)$ ]]; then
|
||||||
|
echo " → \"$text\" (needs this.t())"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# Generate migration instructions
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}📋 Next Steps:${NC}"
|
||||||
|
echo "1. Review the generated translation files in $LOCALES_DIR/"
|
||||||
|
echo "2. Replace placeholder translations (TODO: lang) with proper translations"
|
||||||
|
echo "3. Add data-i18n attributes to hardcoded HTML strings"
|
||||||
|
echo "4. Replace hardcoded JavaScript strings with this.t() calls"
|
||||||
|
echo "5. Test the application in different languages"
|
||||||
|
|
||||||
|
# Clean up temporary files
|
||||||
|
rm -f /tmp/html_keys.txt /tmp/js_keys.txt /tmp/all_keys.txt /tmp/*_new.json
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}✅ Translation update completed!${NC}"
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# Wiktionary Random Word Generator - Verfeinerte Version
|
||||||
|
|
||||||
|
Eine **hochoptimierte Version** des Wiktionary Random Word Generators mit verbesserter Definition-Extraktion und erweiterten Filteroptionen.
|
||||||
|
|
||||||
|
## 🔧 Neue Verfeinerungen
|
||||||
|
|
||||||
|
### Saubere Definition-Extraktion
|
||||||
|
- **Entfernt führende Nummerierungen**: `1]`, `[2]`, `3)` werden automatisch entfernt
|
||||||
|
- **Bereinigt Formatierungen**: Entfernt `:`, `•`, `>`, `(`, `"` am Anfang
|
||||||
|
- **Intelligente Kleinschreibung**: Definitionen beginnen mit Kleinbuchstaben
|
||||||
|
- **Bessere Lesbarkeit**: Cleaner, natürlicher Text ohne Wiki-Markup
|
||||||
|
|
||||||
|
### Zahlen-Filter (Neu)
|
||||||
|
- **`--exclude-numbers` Option**: Schließt Wörter mit Ziffern aus
|
||||||
|
- **Intelligente Erkennung**: Filtert `5-mal`, `Jahr2024`, `3D-Drucker`
|
||||||
|
- **Erhält Wörter**: `dreimal`, `hundert`, `eins` bleiben erlaubt
|
||||||
|
- **Flexible Nutzung**: Optional aktivierbar per Parameter
|
||||||
|
|
||||||
|
## 📋 Verwendung
|
||||||
|
|
||||||
|
### Standard (Empfohlen)
|
||||||
|
```bash
|
||||||
|
# 50 deutsche Wörter mit sauberen Definitionen
|
||||||
|
python wiktionary_random_refined.py -c 50 -l de
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mit Zahlen-Ausschluss
|
||||||
|
```bash
|
||||||
|
# Keine Wörter mit Ziffern (5-mal, Jahr2024, etc.)
|
||||||
|
python wiktionary_random_refined.py -c 30 --exclude-numbers
|
||||||
|
|
||||||
|
# Kombiniert mit Wortart-Filter
|
||||||
|
python wiktionary_random_refined.py -c 25 --include-pos verb adjektiv --exclude-numbers
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erweiterte Kombinationen
|
||||||
|
```bash
|
||||||
|
# Englische Wörter, Namen erlaubt, aber keine Zahlen
|
||||||
|
python wiktionary_random_refined.py -c 40 -l en --include-names --exclude-numbers
|
||||||
|
|
||||||
|
# Nur Substantive und Verben, ohne Zahlen
|
||||||
|
python wiktionary_random_refined.py -c 35 --include-pos substantiv verb --exclude-numbers
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Definition-Bereinigung (Beispiele)
|
||||||
|
|
||||||
|
### Vorher → Nachher
|
||||||
|
```
|
||||||
|
1] eine große Pflanze mit Stamm und Ästen
|
||||||
|
→ eine große Pflanze mit Stamm und Ästen
|
||||||
|
|
||||||
|
[2] übertragen: etwas Großes und Stabiles
|
||||||
|
→ übertragen: etwas Großes und Stabiles
|
||||||
|
|
||||||
|
: [1] positive Eigenschaft oder Zustand
|
||||||
|
→ positive Eigenschaft oder Zustand
|
||||||
|
|
||||||
|
• erste Bedeutung: schön anzusehen
|
||||||
|
→ erste Bedeutung: schön anzusehen
|
||||||
|
|
||||||
|
(1) Gebäude zum Wohnen
|
||||||
|
→ gebäude zum Wohnen
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔢 Zahlen-Filter (Beispiele)
|
||||||
|
|
||||||
|
### Ausgeschlossen (mit --exclude-numbers)
|
||||||
|
- `5-mal` → ❌ (enthält Ziffer)
|
||||||
|
- `Jahr2024` → ❌ (enthält Ziffer)
|
||||||
|
- `3D-Drucker` → ❌ (enthält Ziffer)
|
||||||
|
- `42fach` → ❌ (enthält Ziffer)
|
||||||
|
|
||||||
|
### Erlaubt
|
||||||
|
- `dreimal` → ✅ (keine Ziffern)
|
||||||
|
- `hundert` → ✅ (ausgeschrieben)
|
||||||
|
- `eins` → ✅ (ausgeschrieben)
|
||||||
|
- `Baum` → ✅ (keine Zahlen)
|
||||||
|
|
||||||
|
## 🔧 Alle Parameter
|
||||||
|
|
||||||
|
| Parameter | Kurz | Beschreibung | Standard |
|
||||||
|
|-----------|------|--------------|----------|
|
||||||
|
| `--count` | `-c` | Anzahl Wörter | 50 |
|
||||||
|
| `--language` | `-l` | Sprache (de/en/fr/es/it) | de |
|
||||||
|
| `--output` | `-o` | Output-Datei | refined_wordlist.csv |
|
||||||
|
| `--include-pos` | - | Wortarten-Filter | adjektiv verb substantiv |
|
||||||
|
| `--include-names` | - | Namen einschließen | False |
|
||||||
|
| `--exclude-numbers` | - | **NEU**: Zahlen ausschließen | False |
|
||||||
|
| `--verbose` | `-v` | Ausführliche Ausgabe | - |
|
||||||
|
|
||||||
|
## 📊 Beispiel-Output
|
||||||
|
|
||||||
|
```
|
||||||
|
🎲 Wiktionary Random Word Generator (Verfeinerte Version)
|
||||||
|
===========================================================================
|
||||||
|
Sprache: DE
|
||||||
|
Anzahl: 25
|
||||||
|
Wortarten: adjektiv, verb, substantiv (Standard)
|
||||||
|
Namen einschließen: Nein
|
||||||
|
Zahlen ausschließen: Ja
|
||||||
|
Output: refined_wordlist.csv
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Sammle Wörter aus Kategorien: adjektiv, verb, substantiv
|
||||||
|
Lade adjektiv aus Kategorie:Adjektiv (Deutsch)...
|
||||||
|
→ 167 Wörter gefunden
|
||||||
|
Lade verb aus Kategorie:Verb (Deutsch)...
|
||||||
|
→ 234 Wörter gefunden
|
||||||
|
Lade substantiv aus Kategorie:Substantiv (Deutsch)...
|
||||||
|
→ 445 Wörter gefunden
|
||||||
|
Gesamt gesammelt: 846 Wörter
|
||||||
|
|
||||||
|
Verarbeite Kandidaten...
|
||||||
|
1/25: schön
|
||||||
|
2/25: laufen
|
||||||
|
3/25: Baum
|
||||||
|
[...]
|
||||||
|
25/25: denken
|
||||||
|
|
||||||
|
✅ 25 Wörter erfolgreich generiert!
|
||||||
|
💾 Wortliste gespeichert in: refined_wordlist.csv
|
||||||
|
|
||||||
|
📖 Beispiele aus der generierten Liste:
|
||||||
|
-----------------------------------------------------------------
|
||||||
|
1. schön → ansprechend und angenehm in der Erscheinung...
|
||||||
|
2. laufen → sich zu Fuß mit schnellen Schritten fortbewegen...
|
||||||
|
3. Baum → großes, meist mehrjähriges Gewächs mit Stamm...
|
||||||
|
4. intelligent → geistig begabt, klug und aufgeweckt...
|
||||||
|
5. sprechen → mündlich kommunizieren, Worte äußern...
|
||||||
|
... und 20 weitere Wörter
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Verbesserungen gegenüber vorherigen Versionen
|
||||||
|
|
||||||
|
### Definition-Qualität
|
||||||
|
1. **Sauberer Text**: Keine störenden `1]`, `[2]` Präfixe mehr
|
||||||
|
2. **Natürliche Sprache**: Definitionen lesen sich flüssiger
|
||||||
|
3. **Konsistente Formatierung**: Einheitlicher Stil ohne Wiki-Markup
|
||||||
|
4. **Bessere UX**: Professionellere, verwendbare Ausgabe
|
||||||
|
|
||||||
|
### Filter-Präzision
|
||||||
|
1. **Zahlen-Kontrolle**: Optionaler Ausschluss numerischer Begriffe
|
||||||
|
2. **Flexible Konfiguration**: Alle Filter kombinierbar
|
||||||
|
3. **Intelligente Erkennung**: Unterscheidet Ziffern vs. ausgeschriebene Zahlen
|
||||||
|
4. **Mehrschichtiger Ansatz**: Basis- + Kategorie- + Zahlen-Filter
|
||||||
|
|
||||||
|
### Performance-Optimierungen
|
||||||
|
1. **Effizientere Regex**: Optimierte Pattern für Definition-Bereinigung
|
||||||
|
2. **Reduzierte API-Calls**: Weniger Kategorien-Checks durch Vorfilterung
|
||||||
|
3. **Bessere Kandidaten-Auswahl**: Höhere Erfolgsquote bei finaler Auswahl
|
||||||
|
|
||||||
|
Das verfeinerte Script ist besonders geeignet für:
|
||||||
|
- **Professionelle Anwendungen**: Saubere, verwendbare Definitionen
|
||||||
|
- **Sprachlernen**: Klare, verständliche Erklärungen ohne Formatierungs-Chaos
|
||||||
|
- **Datenqualität**: Konsistente, strukturierte Ausgabe
|
||||||
|
- **Flexible Filterung**: Präzise Kontrolle über Inhaltstypen
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
meiden transitiv, gehoben: etwas dafür tun, einer Person oder Sache nicht zu begegnen
|
||||||
|
sturmgepeitscht gehoben: vom Sturm heftig bewegt, aufgewirbelt und/oder geschleudert werdend
|
||||||
|
Tonkünstlerin gehoben, Musik: Künstlerin, die musikalische Werke schafft
|
||||||
|
duress gehoben, meist Rechtssprache: Nötigung, Zwang
|
||||||
|
Wohlgestalt gehoben veraltend: schöne, ansprechende Gestalt
|
||||||
|
Gezweig gehoben: Gesamtheit aller Zweige (einer Hecke, eines Baumes oder Ähnlichem)
|
||||||
|
hinabwerfen transitiv; gehoben: von (hier) oben nach (dort) unten werfen (weg vom Sprechenden)
|
||||||
|
Verblichener gehoben: männliche Person, die gestorben ist
|
||||||
|
aufkündigen einen Vertrag/eine Vertragsbeziehung beenden
|
||||||
|
straßauf, straßab adverbiell; gehoben: auf/in vielen Straßen, durch mehrere Straßen
|
||||||
|
entströmen intransitiv, unpersönlich, gehoben: aus etwas in Strömen herauskommen
|
||||||
|
durchkreuzen transitiv: Wörter, Buchstaben in einem Text mit Kreuzen (×) unleserlich, ungültig machen
|
||||||
|
nimmermüde gehoben: nicht ermüdend, unermüdlich
|
||||||
|
Heermeister gehoben oder veraltet: Oberbefehlshaber (eines Teils) der Streitkräfte, eines Heeres
|
||||||
|
Frieden kein Plural: vertraglich gesichertes Miteinander verschiedener Staaten, das durch Abwesenheit von Gewalt (und speziell von Krieg) gekennzeichnet ist
|
||||||
|
späterhin gehoben: zu einem Zeitpunkt, der nach einem Ereignis liegt, auf etwas folgt, noch in der Zukunft liegt
|
||||||
|
Stachel im Fleisch gehoben: immerwährende eindringliche Erinnerung an etwas
|
||||||
|
Tondichterin gehoben, Musik: Künstlerin, die musikalische Werke schafft
|
||||||
|
Verstorbene gehoben: weibliche Person, die nicht mehr am Leben, sondern tot ist
|
||||||
|
übersonnt gehoben; auch übertragen: von der Sonne hell erleuchtet, von Sonnenstrahlen beschienen
|
||||||
|
engen transitiv, selten, gehoben: etwas schmäler machen
|
||||||
|
hungrig ein Gefühl des Hungers verspürend, Hunger habend
|
||||||
|
Dotation gehoben: finanzielle und/oder materielle Zuwendung
|
||||||
|
emporgehen nach oben gehen
|
||||||
|
verstummen intransitiv, von Personen: (plötzlich) aufhören, die Stimme zu gebrauchen
|
||||||
|
unflätig gehoben, abwertend: so, dass es nicht den üblichen Regeln des guten Benehmens oder Anstandes entspricht
|
||||||
|
Widerfahrnis veraltet, gehoben, sonst fachsprachlich, Psychologie: dasjenige (nicht Beeinflussbare), was (einem, jemandem) widerfährt
|
||||||
|
Fittich poetisch, gehoben: Körperteil, das einem Tier das Fliegen ermöglicht
|
||||||
|
Trottoir österreichisch gehoben, schweizerisch, süddeutsch, sonst veraltend oder landschaftlich: (durch eine Bordsteinkante von der Fahrbahn abgegrenzter, erhöhter) Weg für Fußgänger und Fußgängerinnen, der entlang einer Straße führt
|
||||||
|
durchpflügen transitiv: eine Fläche sehr stark und gründlich mit dem Pflug bearbeiten
|
||||||
|
in Flammen aufgehen gehoben: durch Feuer vollständig zerstört werden, verbrennen
|
||||||
|
ekel gehoben, sonst veraltend: (moralisch oder physisch) Ekel erregend
|
||||||
|
Bildnerin veraltet: weibliche Person, die eine Person oder Sache formt, ausbildet
|
||||||
|
guter Dinge in einer heiteren, vergnügten Stimmung
|
||||||
|
bisweilen gehoben: unterschiedlich häufig, mehr oder minder oft
|
||||||
|
herniederbrennen intransitiv, gehoben, Hilfsverb haben, von der Sonne: große Hitze, gleißendes Licht nach unten ausstrahlen
|
||||||
|
beargwöhnen gehoben: misstrauisch betrachten
|
||||||
|
partizipieren gehoben: an etwas teilhaben; an etwas seinen Anteil haben
|
||||||
|
zurücksenden transitiv; gehoben: etwas wieder an einen bestimmten Ort/zu einer bestimmten Person gelangen lassen
|
||||||
|
dahinwelken intransitiv; gehoben: vom blühenden in einen welken Zustand übergehen
|
||||||
|
anbequemen reflexiv, veraltend, sonst gehoben: sich mit seinem Verhalten, seinen Handlungen an jemandem oder etwas orientieren, sich danach richten
|
||||||
|
epochal gehoben: von großer Bedeutung, bedeutsam für eine Epoche
|
||||||
|
heimführen jemanden in dessen Zuhause bringen
|
||||||
|
ausharren intransitiv; gehoben: (trotz Widrigkeiten) mit Geduld abwarten, warten
|
||||||
|
emporwirbeln transitiv, Hilfsverb haben: kleine Teilchen (zum Beispiel Sand, Schnee) so in Bewegung versetzen, dass sie wild drehend, ungeordnet nach oben fliegen
|
||||||
|
pasteur gehoben: Hirt, Senner
|
||||||
|
Landesvater gehoben, historisch: Fürst eines Landes
|
||||||
|
Tonschöpfer gehoben, Musik: Künstler, der musikalische Werke schafft
|
||||||
|
zurückbegehren transitiv, gehoben: etwas wieder in seinem Besitz, bei sich haben wollen
|
||||||
|
Tonmaler gehoben, Musik: Künstler, der musikalische Werke schafft
|
||||||
|
erfließen österreichisch, Amtssprache, gehoben: daraus folgen, sich ergeben, entstehen
|
||||||
|
einmütig gehoben: hinsichtlich der Meinung übereinstimmend, völlig gleich
|
||||||
|
Lustbarkeit gehoben, veraltend: Veranstaltung, die jemand zur Unterhaltung, zur Vergnügung besucht; allgemein auch: etwas, was Freude bereiten soll
|
||||||
|
Ansinnen gehoben: oft als Zumutung empfundenes Ersuchen, Gesuch, Bitte
|
||||||
|
Dämmer gehoben: (Tages-)Licht, das allmählich hell beziehungsweise dunkel wird
|
||||||
|
hinabgehen intransitiv, gehoben: nach unten gehen
|
||||||
|
wohlauf gehoben: gesund, in guter Verfassung
|
||||||
|
hinausschauen regional: aus einem bestimmten Bereich, von einer bestimmten Stelle (hier drinnen) in einen anderen Bereich, an eine andere Stelle (dort draußen) blicken (schauen) (in Richtung vom Sprecher weg)
|
||||||
|
äquivalent gehoben: gleichwertig, entsprechend
|
||||||
|
Anverwandter gehoben: Person, die zur Verwandtschaft gehört
|
||||||
|
den Tod finden gehoben: infolge von etwas (Unfall, Krankheit, Ermordung oder dergleichen) aufhören zu leben
|
||||||
|
mit doppelter Zunge sprechen gehoben: Unwahres oder Widersprüchliches sagen; heuchlerisch reden; fuchsschwänzen
|
||||||
|
Verelendung gehoben: das Geraten in sehr schlechte Lebensumstände
|
||||||
|
obzwar gehoben, veraltend: leitet einen Konzessivsatz ein, der ein Gegenargument gegen das zuvor Gesagte enthält; obwohl
|
||||||
|
Bacchus huldigen gehoben, euphemistisch: Wein trinken
|
||||||
|
Edelmut gehoben: edle Gesinnung, die egoistische Motive hintenan stellt
|
||||||
|
hoffärtig gehoben abwertend: besonders hochmütig, andere Personen verletzend
|
||||||
|
sonach gehoben: eine Folgerung einleitend: folglich, also
|
||||||
|
Todespein gehoben: tödliche Qualen, Schmerzen
|
||||||
|
weiterreichen transitiv, gehoben: etwas, das man von jemandem erhalten hat, einem anvertraut wurde, einem anderen (händisch) geben, in dessen Verantwortung überstellen
|
||||||
|
Mund Anatomie: Öffnung im unteren Teil des (hauptsächlich menschlichen) Gesichtes, hauptsächlich zur Nahrungsaufnahme und zur Lautbildung genutzt
|
||||||
|
Notschrei gehoben, veraltend: lauter Ruf nach Hilfe aus einer Notsituation
|
||||||
|
nicken intransitiv: (zur Bejahung, Zustimmung, als Geste des Verstehens, als Gruß oder dergleichen) den Kopf kurz hintereinander senken und heben
|
||||||
|
herabkommen intransitiv, gehoben: (von dort oben) nach (hier) unten kommen (in Richtung zum Sprechenden)
|
||||||
|
voranschreiten vor jemand anders gehen
|
||||||
|
Anverwandte gehoben: weibliche Person, die zur Verwandtschaft gehört
|
||||||
|
betören gehoben: jemanden faszinieren, in sich verliebt machen
|
||||||
|
Unnatur gehoben: etwas, das der Natur/der natürlichen Veranlagung nicht gemäß ist
|
||||||
|
winden transitiv: etwas drehen, wickeln
|
||||||
|
durchtosen transitiv, gehoben, veraltend: mit heftiger, wilder Bewegung und dröhnendem Geräusch durchqueren
|
||||||
|
feilbieten transitiv, gehoben: etwas zum Kauf anbieten
|
||||||
|
nachgerade umgangssprachlich, mit Betonung auf Ende/Konsequenz eines Geschehens/Prozesses: endlich, letztlich
|
||||||
|
edelgeboren veraltet gehoben: von vornehmer/adliger Herkunft
|
||||||
|
mit gespaltener Zunge reden gehoben: Unwahres oder Widersprüchliches sagen, heuchlerisch reden, fuchsschwänzen
|
||||||
|
emporarbeiten sich unter Anstrengungen, Mühen von einer unten gelegenen Stelle nach oben bewegen (vom Sprecher weg)
|
||||||
|
anfliegen in einem Luftfahrzeug, mit Flügeln, durch die Luft näher kommen
|
||||||
|
auserlesen gehoben: qualitativ besonders gut/hochwertig
|
||||||
|
goldig selten, gehoben: die Farbe und den Glanz von Gold habend (auch durch Vergolden)
|
||||||
|
fernhin gehoben: in weitem Umkreis, bis in weite Ferne
|
||||||
|
Abgeschiedener gehoben: (kürzlich) verstorbene (männliche) Person
|
||||||
|
Wehe veraltet gehoben: seelischer Schmerz
|
||||||
|
Beilager gehoben veraltet: sexuelle Vereinigung
|
||||||
|
antizipieren gehoben, bildungssprachlich: etwas erkennen, bevor es eingetreten ist
|
||||||
|
mit gespaltener Zunge sprechen gehoben: Unwahres oder Widersprüchliches sagen; heuchlerisch reden; fuchsschwänzen
|
||||||
|
Trottoirfläche österreichisch gehoben, schweizerisch, süddeutsch, sonst veraltend oder landschaftlich: befestigte und meist gepflasterte Fläche am Rand einer Straße, die für Fußgänger vorgesehen ist
|
||||||
|
Kronjuwel kostbares Schmuckstück, das sich im Besitz des Herrscherhauses befindet
|
||||||
|
gürten gehoben: jemandem/sich einen Gürtel oder eine andere Art Riemen oder Band umlegen, umschnallen; etwas mit einem Gürtel oder Ähnlichem versehen
|
||||||
|
leichten Herzens gehoben: ohne dass es jemandem Anstrengung, Sorgen bereitet, jemanden Überwindung kostet, ohne Bedenken, Zweifel; sehr gern; völlig unbekümmert
|
||||||
|
ersterben aufhören
|
||||||
|
empordienen reflexiv, gehoben: sich durch Fleiß, beständiges Handeln/Tätigsein im Beruf oder Ähnliches nach und nach stufenweise in eine höhergestellte Position bringen
|
||||||
|
Augenweite gehoben oder veraltet: mit den Augen einsehbare Entfernung
|
||||||
|
aufblühen intransitiv: sich blühend öffnen, die Blüten (langsam) auseinanderfalten; anfangen, sich zu voller Blüte zu entfalten, allmählich in Blüte kommen
|
||||||
|
abdarben transitiv, gehoben, sich etwas abdarben: sich etwas (bis zum Grad des Notleidens) entziehen (um es für Anderweitiges aufzusparen)
|
||||||
|
tagen gehoben: Tag werden
|
||||||
|
anreihen transitiv: (etwas) einer bereits vorhandenen Reihe hinzufügen
|
||||||
|
aufbrechen auch bildlich: etwas, das verschlossen ist, mit Gewalt öffnen
|
||||||
|
geizen mit der Präposition „mit“: in übertriebener, unangemessener Weise sparen, geizig mit etwas sein
|
||||||
|
umfriedigen gehoben, selten: einrahmend, abgrenzend umgeben
|
||||||
|
niederstrecken transitiv; gehoben, auch übertragen: einen Menschen oder ein Tier schwer verletzen oder töten und so zu Boden bringen
|
||||||
|
ausgleiten intransitiv; auch übertragen: auf einer glatten Fläche aus dem Gleichgewicht kommen, keinen Halt finden (und hinfallen)
|
||||||
|
distinguiert gehoben: vornehm; sich durch betont gepflegtes Auftreten oder Ähnliches von anderen abhebend
|
||||||
|
Kunde jemand, der bei einem bestimmten Geschäft einkauft; der Käufer einer Ware; derjenige, der eine Dienstleistung in Anspruch nimmt; jeder, der für etwas zahlt (auch wenn die Leistung an einen Dritten geht)
|
||||||
|
Allgütiger gehoben: derjenige, der allgütig ist: Gott
|
||||||
|
Gemahl gehoben: der vermählte Mann
|
||||||
|
die Hülle und Fülle gehoben: in großer Menge, im Überfluss
|
||||||
|
Freveltat gehoben: einzelne Handlung, die einen Frevel gegen etwas darstellt; frevelhafte Tat
|
||||||
|
beschatten gehoben: jemandem/etwas einen Schatten verleihen
|
||||||
|
schmachbeladen gehoben: sehr erniedrigt, in seiner Würde stark herabgesetzt, in seinem Stolz sehr verletzt, Schande auf sich geladen habend
|
||||||
|
entschwinden intransitiv, gehoben: sich von etwas entfernen
|
||||||
|
allein als Adjektiv: für sich, getrennt, einsam, einzeln sein; nur; ohne Beistand, Zeugen, Anwesenheit von anderen Personen, ohne Hilfe sein
|
||||||
|
zurückverlangen von jemandem die Rückgabe von etwas fordern, das einem gehört
|
||||||
|
ergötzlich gehoben: in unterhaltsamer Art und Weise Gefallen/Vergnügen bereitend
|
||||||
|
erkühnen reflexiv, gehoben: so wagemutig und frech (kühn) sein, dass man etwas eigentlich Unerlaubtes/Unzulässiges versucht
|
||||||
|
dergestalt gehoben: derart, dermaßen
|
||||||
|
anfechten die Gültigkeit/Rechtmäßigkeit einer Entscheidung anzweifeln und dann dagegen vorgehen
|
||||||
|
über Gebühr gehoben: über das angemessene Maß hinausgehend, mehr als erforderlich
|
||||||
|
Hall gehoben: akustisch wahrnehmbares, unspezifisches Ereignis
|
||||||
|
fortrühren gehoben, reflexiv, meistens verneint: sich wegbewegen; sich vom eigenen Aufenthaltsort entfernen
|
||||||
|
Inbrunst gehoben: starke Hingabe an ein Gefühl oder an ein Vorhaben
|
||||||
|
anspeien transitiv, gehoben: jemanden oder etwas (absichtlich) mit Spucke oder Erbrochenem beschmutzen
|
||||||
|
Begräbnisstätte gehoben: Stelle, wo jemand begraben liegt
|
||||||
|
Land der Morgenröte poetisch, gehoben: Bezeichnung und Selbstbezeichnung für China
|
||||||
|
Beute durch Diebstahl, Raub oder Plünderung angeeignete Güter
|
||||||
|
Diskutant gehoben: Person, die sich an einer Diskussion beteiligt
|
||||||
|
bedürfen gehoben, mit Genitiv: etwas benötigen, etwas brauchen, auf etwas angewiesen sein
|
||||||
|
entsühnen transitiv, auch reflexiv, gehoben: jemanden, sich durch Sühne von Sünde, Schuld befreien
|
||||||
|
Degout gehoben: starke Abneigung gegen jemanden oder etwas
|
||||||
|
entzünden transitiv: in Brand setzen
|
||||||
|
Quisquilie gehoben, meist Plural: Kleinigkeit, Nichtigkeit, Bagatelle
|
||||||
|
äffen transitiv, gehoben, veraltete Bedeutung: jemanden durch Unwahrheiten irreführen, täuschen
|
||||||
|
hernieder gehoben: aus der Richtung von irgendwo oben nach unten, meist zum Sprecher, Erzähler oder Handelnden hin
|
||||||
|
so weit das Auge reicht gehoben: so weit man blicken/sehen kann; überall
|
||||||
|
einstmals gehoben: früher, vor langer Zeit
|
||||||
|
sinnesfroh gehoben: mit Sinnenfreude; sich am sinnlichen Genuss erfreuend
|
||||||
|
nachforschen intransitiv: sich intensiv mit dem Sammeln von Informationen über etwas beschäftigen
|
||||||
|
abspeisen transitiv, umgangssprachlich: jemandes Hunger stillen
|
||||||
|
umfrieden gehoben: einrahmend, abgrenzend umgeben
|
||||||
|
Wegesrand gehoben: seitliche Grenze eines Weges
|
||||||
|
rapide gehoben, oft bildlich: äußerst schnell
|
||||||
|
nachkommen intransitiv: jemandem, einer Sache nach einiger Zeit folgen; einige Zeit später hinterherkommen; auf etwas (später) als Konsequenz folgen
|
||||||
|
zuwider gehoben, jemandem ist etwas zuwider: Ablehnung und unangenehme Gefühle hervorrufend
|
||||||
|
divinatorisch gehoben: die Fähigkeit zur Voraussage von Ereignissen besitzend
|
||||||
|
wohlgemut gehoben: heiter gestimmt
|
||||||
|
Affront gehoben: öffentlicher verbaler oder schriftlicher Angriff bzw. Beschuldigung
|
||||||
|
zurücksehnen reflexiv: wieder an einen bestimmten Ort/in eine bestimmte Zeit/zu einer bestimmten Person zurückversetzt sein wollen
|
||||||
|
zuinnerst sehr tief in der Gedanken- und Gefühlswelt einer Person
|
||||||
|
zu Tode kommen gehoben: infolge von Unfall, Krankheit, Ermordung oder dergleichen aufhören zu leben
|
||||||
|
erwürgen jemanden ermorden, indem man ihm (mit den Händen) den Hals zusammendrückt, um die Luftzufuhr abzuschneiden, sodass das Blut nicht mehr zum Kopf zirkulieren kann
|
||||||
|
überstrahlen transitiv, gehoben: auf der gesamten Fläche bestrahlen
|
||||||
|
mitnichten veraltend, gehoben: auf keinen Fall, in keiner Weise
|
||||||
|
fortsterben intransitiv, gehoben oder veraltete Bedeutung: (allmählich, aber auch plötzlich) aufhören zu leben
|
||||||
|
zurückbeben intransitiv, gehoben oder veraltete Bedeutung: mit einer kurzen, heftigen Bewegung vor etwas oder jemandem zurückweichen
|
||||||
|
mit doppelter Zunge reden gehoben: Unwahres oder Widersprüchliches sagen, heuchlerisch reden, fuchsschwänzen
|
||||||
|
spartanisch keine Steigerung: das antike Sparta betreffend
|
||||||
|
Dolmetsch besonders in Österreich: Person, die (gesprochene) Texte in eine andere Sprache übersetzt
|
||||||
|
Unflat gehoben, veraltend: abstoßend wirkender Schmutz
|
||||||
|
ausweinen reflexiv: (bei jemandem) Erleichterung suchen durch Weinen, sich jemandem anvertrauen, jemandem von seinen Sorgen erzählen
|
||||||
|
vorwalten gehoben, veraltend: vorhanden sein; wirksam sein
|
||||||
|
seligen Angedenkens veraltet bis veraltend, sonst gehoben verhüllend: Beistellung, die die erwähnte Person als verstorben kennzeichnet
|
||||||
|
Reich der Mitte poetisch, gehoben, literatursprachlich: Bezeichnung und Selbstbezeichnung für China; wörtliche Übersetzung der chinesischen Schriftzeichen: zhong guo; wird insbesondere historisch und außerhalb Chinas in Poesie und Prosa verwendet
|
||||||
|
Strahlkraft gehoben: starke Wirkung auf andere
|
||||||
|
hinausgeleiten transitiv; gehoben: jemanden (von hier drinnen) nach dort draußen begleiten/führen (weg vom Sprechenden)
|
||||||
|
umwittern veraltet: mit schlechtem Wetter oder Unwetter umgeben
|
||||||
|
hoffnungsfroh gehoben: der Zukunft zuversichtlich entgegensehend
|
||||||
|
Zutrunk gehoben veraltet: das Erheben eines (mit Alkohol gefüllten) Glases, um einer Person die Ehre zu erweisen oder ihr Gutes zu wünschen
|
||||||
|
Akteur gehoben: Person, die handelt, die etwas tut
|
||||||
|
ungesättigt gehoben: Hunger verspürend
|
||||||
|
Weh gehoben: seelischer Schmerz
|
||||||
|
martervoll gehoben: mit großen Schmerzen, nach schmerzhaften Qualen
|
||||||
|
vermessen übermäßig ehrgeizig und deshalb riskant
|
||||||
|
heißen genannt werden, den Namen haben
|
||||||
|
Sinnenrausch gehoben: rauschhafter Zustand der Sinne
|
||||||
|
Mehrer gehoben: Person, die eine größere Menge von etwas anhäuft
|
||||||
|
dahinschwinden intransitiv, gehoben, unpersönlich: (allmählich) weniger werden
|
||||||
|
voreinst gehoben oder veraltet: vor langer/längerer Zeit
|
||||||
|
niederlassen veraltende Bedeutung: einen Gegenstand senken/auf den Boden legen
|
||||||
|
einhellig gehoben: ohne Gegenstimmen
|
||||||
|
bekleiden transitiv, reflexiv: mit Bekleidung versehen
|
||||||
|
Blattkrone gehoben: Baumkrone
|
||||||
|
widereinander gehoben, von Personen oder Dingen: im Widerstreit zueinander, in einem konkurrierenden Verhältnis zueinander
|
||||||
|
Mondesfinsternis Astronomie, Österreich, gehoben: Ereignis, bei dem der Mond durch den Kernschatten der Erde wandert
|
||||||
|
Gezweige gehoben: Gesamtheit aller Zweige (einer Hecke, eines Baumes oder Ähnliches)
|
||||||
|
Apotheose Religion, gehoben: die Erklärung eines Menschen zu einer Gottheit
|
||||||
|
Œuvre gehoben, bildungssprachlich: Gesamtheit aller Einzelwerke eines Künstlers
|
||||||
|
bejahrt gehoben: in vorgerücktem Alter
|
||||||
|
mondhell gehoben: durch das vom Erdmond reflektierte Licht erhellt
|
||||||
|
Ichsucht gehoben, veraltend: Haltung, die darauf aus ist, sich selbst immer zuerst zu berücksichtigen
|
||||||
|
gebrechen gehoben, unpersönlich, jemandem (Dativ) gebricht es an etwas (Dativ); persönlich nur veraltend: fehlen, mangeln
|
||||||
|
Furor gehoben: Wildheit, Raserei
|
||||||
|
Bringerin veraltend, gehoben: Überbringerin von etwas
|
||||||
|
Billigkeit Eigenschaft, preisgünstig zu sein
|
||||||
|
abdorren gehoben: trocken werden (und dadurch absterben)
|
||||||
|
Lüsternheit gehoben: Zustand, lüstern/sexuell begehrlich zu sein
|
||||||
|
Buhle gehoben, veraltet: Geliebter
|
||||||
|
aufzehren transitiv, auch übertragen: etwas vollständig verzehren, aufbrauchen
|
||||||
|
landauf, landab adverbiell; gehoben: im ganzen Land
|
||||||
|
kundmachen transitiv, österreichische Amtssprache, sonst gehoben: öffentlich bekanntgeben
|
||||||
|
affirmativ gehoben: Zustimmung/Affirmation erkennen lassend
|
||||||
|
meinige gehoben, veraltend: Nebenform von mein
|
||||||
|
Leichnam gehoben: Körper eines Verstorbenen
|
||||||
|
rotwangig gehoben: mit roten Wangen
|
||||||
|
blicklos gehoben: mit offenen Augen, aber nichts wahrnehmend
|
||||||
|
delegitimieren gehoben: jemanden oder etwas rechtlich nicht anerkennen; die Legitimation von etwas oder jemandem aufheben
|
||||||
|
ephemerisch gehoben: nur einen Tag dauernd, nur kurze Zeit bestehend, rasch vorübergehend
|
||||||
|
fortan gehoben: ab jetzt, von jetzt an, ab sofort, in Zukunft
|
||||||
|
entbehren transitiv, gehoben: das Nichtvorhandensein einer Person beziehungsweise einer Sache als persönlichen Mangel empfindend erdulden müssen
|
||||||
|
grosso modo gehoben: insgesamt betrachtet
|
||||||
|
nottun gehoben, noch landschaftlich: nötig, vonnöten sein
|
||||||
|
Aberwitz gehoben: etwas völlig (absurd) Unsinniges; Handeln oder Verhalten, das abwegig, unvernünftig, bisweilen zudem gefährlich ist
|
||||||
|
junonisch gehoben: sehr schön (wie die Göttin Juno)
|
||||||
|
unziemlich gehoben: den sozialen Normen nicht entsprechend
|
||||||
|
anbefehlen jemandem etwas ausdrücklich befehlen, dringend anraten
|
||||||
|
zurückgelangen intransitiv, gehoben: wieder an einem bestimmten Ort/bei einem bestimmten Ziel eintreffen, erneut zu diesem kommen
|
||||||
|
guten Mutes sein gehoben: in einer fröhlichen Stimmung sein; voller Zuversicht sein
|
||||||
|
Sturmwind dichterisch, gehoben: mit großer Wucht einhergehender, sehr starker Wind; Sturm
|
||||||
|
freund gehoben veraltend: freundlich gegenüber jemandem/etwas eingestellt, Zuneigung gegenüber jemandem/etwas hegend
|
||||||
|
aufrühren durch Umrühren etwas nach oben steigen lassen
|
||||||
|
hinuntergeleiten transitiv, gehoben: (jemanden) von einem höhergelegenen Ort nach unten führen, begleiten (vom Sprecher weg)
|
||||||
|
alert gehoben: munter, aufgeweckt, rege, frisch
|
||||||
|
kontrafaktisch gehoben: gegen das Gängige und Gewohnte gerichtet
|
||||||
|
ausatmen intransitiv: die Atemluft ausstoßen
|
||||||
|
jüngsthin gehoben oder veraltet: vor Kurzem, vor nicht langer Zeit
|
||||||
|
Himmelsbote dichterisch, gehoben: zwischen Himmel und Erde verkehrendes übernatürliches Wesen als Überbringer göttlicher Botschaften
|
||||||
|
sonnenbeschienen gehoben: im Sonnenschein befindlich
|
||||||
|
dahinsterben intransitiv, gehoben: (allmählich, aber auch plötzlich) aufhören zu leben
|
||||||
|
Tondichter gehoben, Musik: Künstler, der musikalische Werke schafft
|
||||||
|
entraten veraltet, gehoben: auf etwas Verzicht üben, ohne etwas zurechtkommen
|
||||||
|
seimig veraltet, gehoben: so, dass es nur langsam und schwer fließt; von eher zäher Konsistenz
|
||||||
|
Segen bringend gehoben: Positives bewirkend, Vorteile für viele bietend
|
||||||
|
Gepflogenheit gehoben: eine nicht verbindliche Übereinkunft, die durch häufiges Wiederholen zur Konvention geworden ist
|
||||||
|
Dulderin gehoben: weibliche Person, die etwas duldet/erträgt
|
||||||
|
zurückbegeben reflexiv, gehoben: den Ort wieder aufsuchen, von dem man kommt, aufgebrochen ist
|
||||||
|
Nebelmeer gehoben, dichterisch: dicht und weit wallender Nebel
|
||||||
|
Strauß (kunstvoll gestaltete, häufig gebundene) Zusammenstellung von abgeschnittenen oder gepflückten Blumen, Halmgräsern, Zweigen oder dergleichen
|
||||||
|
Tonschöpferin gehoben, Musik: Künstlerin, die musikalische Werke schafft
|
||||||
|
weh gehoben: erfüllt, geprägt von Kummer, Leid, (seelischem) Schmerz, Weh
|
||||||
|
herzgeliebt gehoben: sehr, innig geliebt
|
||||||
|
Hort dichterisch, sonst veraltet: (vor allem aus Gold beziehungsweise goldenen Gegenständen zusammengesetzter) Schatz
|
||||||
|
Abendvisite Arztbesuch während der Abendzeit
|
||||||
|
rechten intransitiv oder mit Präpositionalobjekt; gehoben: sich (verbal) mit jemandem über oder um etwas streiten (um sein Recht zu behaupten); für sein Recht einstehen, sein Recht von jemandem fordern
|
||||||
|
anschicken reflexiv, gehoben: sich dazu bereit machen, etwas zu tun; im Begriff sein, etwas zu tun
|
||||||
|
lüstern begierig nach etwas
|
||||||
|
eingeben jemandem etwas (vor allem eine Arznei in einer bestimmten festgesetzten Menge) zum Hinunterschlucken langsam in den Mund geben, jemanden etwas vorsichtig einnehmen lassen
|
||||||
|
flocken intransitiv, Hilfsverb haben, Hilfsverb sein, gehoben: Flocken bilden; flockig werden
|
||||||
|
Abendglanz gehoben: glänzendes und strahlendes Licht während der Abendstunden, insbesondere in Verbindung mit einem schönen Abendhimmel
|
||||||
|
abkargen reflexiv, veraltet, gehoben: etwas, meist Geld, unter Verzicht auf anderes für die spätere Verwendung ansammeln
|
||||||
|
ohne Zahl gehoben: sehr viele; in solch einer Fülle, die sich nicht zählen lässt
|
||||||
|
entschlagen mit Genitiv, veraltet, selten, gehoben, reflexiv: sich innerlich frei machen von einer Sache, sich von etwas befreien, auf etwas verzichten
|
||||||
|
reparabel gehoben: so beschaffen, dass es repariert werden kann
|
||||||
|
aberhundert gehoben: mehrere hundert, noch einmal hundert
|
||||||
|
ein Kind des Todes sein gehoben: sterben müssen; (angesichts einer Todesgefahr) bald sterben werden
|
||||||
|
Sukkus Österreich, gehoben: Zusammenfassung des Wesentlichen; Ergebnis
|
||||||
|
hineilen zu einem bestimmten Ort laufen; sich schnell wegbewegen
|
||||||
|
Fürsorger veraltend: Person, die sich im Auftrag einer Fürsorgeeinrichtung beruflich um Hilfsbedürftige kümmert
|
||||||
|
verhehlen transitiv, gehoben: jemandem etwas (besonders Gefühle und Gedanken) verheimlichen, verbergen
|
||||||
|
Mildtätigkeit gehoben: Bereitschaft, sich um in Not befindliche Menschen zu kümmern oder (meist finanziell) großzügig zu unterstützen
|
||||||
|
zuweilen gehoben: zu bestimmten Zeiten, aber nicht regelmäßig; für eine Weile und später wieder für eine Weile
|
||||||
|
Walplatz gehoben oder veraltet: Platz, an dem ein kriegerischer Kampf stattfindet/stattgefunden hat
|
||||||
|
Mägdlein dichterisch, gehoben oder veraltet: junge weibliche Person
|
||||||
|
erdreisten reflexiv, gehoben: sich etwas herausnehmen, etwas Unerwünschtes tun
|
||||||
|
profund gehoben: gründlich, tiefgreifend, ganz umfassend
|
||||||
|
wehret den Anfängen gehoben: Mahnung, bereits frühzeitig gegen (für die Gesellschaft) gefährliche Entwicklungen, Strömungen vorzugehen
|
||||||
|
augenscheinlich gehoben: (auf den ersten Blick) klar erkennbar, klar ersichtlich
|
||||||
|
anmutlos gehoben veraltet: ohne jegliche Grazie, Eleganz
|
||||||
|
Applikation veraltete Bedeutung: ein Gesuch, eine Bittschrift
|
||||||
|
dem Bacchus huldigen gehoben, euphemistisch: Wein trinken
|
||||||
|
damasten attributiv, gehoben: aus Damast gemacht
|
||||||
|
segenbringend gehoben: Positives bewirkend, Vorteile für viele bietend
|
||||||
|
entfliehen intransitiv: aus einer Gefahr, aus einer Zwangslage entkommen
|
||||||
|
ein Raub der Flammen werden gehoben: durch Feuer vollständig zerstört werden, verbrennen
|
||||||
|
einstweilen gehoben: fürs Erste, bis auf Weiteres
|
||||||
|
dürsten intransitiv, gehoben: Durst haben
|
||||||
|
Mägdelein dichterisch gehoben oder veraltet: junge weibliche Person
|
||||||
|
Verstorbener gehoben: Person, die verstorben ist
|
||||||
|
gottigkeit österreichisch, gehoben, veraltet, zur Beteuerung, parenthetisch verwendet: gewissermaßen, sozusagen, gleichsam, wirklich, bei Gott, will sagen
|
||||||
|
hintanhalten gehoben, sonst besonders Österreich: (durch entsprechende Maßnahmen) bewirken, dass etwas (für eine Weile) nicht geschehen kann, von jemandem (für eine Weile) nicht getan werden kann
|
||||||
|
röten transitiv, gehoben: etwas rot färben, rot erscheinen lassen
|
||||||
|
anrühren jemand oder etwas anfassen, berühren
|
||||||
|
verdämmern intransitiv, gehoben: untätig sein, nicht teilnehmen, keine Anteilnahme, kein Mitgefühl zeigen
|
||||||
|
anwandeln transitiv, gehoben: (jemanden) unvermittelt erfassen, (jemandem) plötzlich in den Sinn kommen (eine Empfindung, Erinnerung, Idee oder Anderes)
|
||||||
|
Ungestüm gehoben: überraschende, nicht logisch erklärbare Wildheit
|
||||||
|
zagen intransitiv, gehoben: ängstlich zögern, bangen
|
||||||
|
wild Botanik, Zoologie, Gegenwort zu zahm oder veredelt: nicht gezähmt; frei in der Natur vorkommend; nicht veredelt
|
||||||
|
türein, türaus adverbiell; gehoben: durch eine Tür hinein und (durch dieselbe oder eine andere) wieder hinaus; von einem Raum, Zimmer, einer Amtsstube, Haustür (oder dergleichen) zur nächsten
|
||||||
|
erquicklich gehoben: so, dass es Freude, Gefallen bereitet, erweckt
|
||||||
|
Hymnus Keine Definition gefunden
|
||||||
|
Toilette eine sanitäre Vorrichtung oder Anlage zur Aufnahme von Körperausscheidungen
|
||||||
|
schweren Herzens gehoben: so dass es jemandem Anstrengung, Sorgen bereitet, jemanden Überwindung kostet; mit Bedenken, Zweifel; sehr ungern; ganz bekümmert; im Widerspruch zu jemandes Gefühl
|
||||||
|
einstens vor langer/längerer Zeit
|
||||||
|
immerdar gehoben, poetisch: zu jedem Zeitpunkt in der Zukunft
|
||||||
|
andachtsvoll gehoben: in Andacht versunken, am Gebet/Gottesdienst/einem spirituellen Inhalt innerlich beteiligt
|
||||||
|
temperieren die Temperatur von etwas so verändern, wie sie gewünscht wird/wie sie als angenehm/passend empfunden wird
|
||||||
|
unerfindlich gehoben: sich nicht erklären lassend, rätselhaft erscheinend
|
||||||
|
in Rauch und Flammen aufgehen gehoben: durch Feuer vollständig zerstört werden, verbrennen
|
||||||
|
Stachel im Fleische gehoben: immerwährende eindringliche Erinnerung an etwas
|
||||||
|
Disputant gehoben: Person, die sich an einem Disput beteiligt
|
||||||
|
Dafürhalten gehoben: Einstellung, dass etwas richtig/zutreffend ist
|
||||||
|
Küchlein gehoben, veraltet: Jungtier von Geflügel
|
||||||
|
anempfinden reflexiv, gehoben: sich in jemandes Gefühlswelt hineinversetzen und sich dessen Empfindungen zu eigen machen
|
||||||
|
unter Waffen stehen gehoben: bewaffnet (und kampfbereit) sein
|
||||||
|
schärfen transitiv: etwas scharf oder schärfer machen
|
||||||
|
au contraire gehoben: im Gegenteil
|
||||||
|
fehlschießen gehoben: einen Schuss abgeben, der das Ziel verfehlt
|
||||||
|
überliefern etwas, beispielsweise eine Geschichte, einen Brauch, an die Nachkommen weitererzählen/weitergeben
|
||||||
|
Dolmetschin besonders in Österreich: weibliche Person, die (gesprochene) Texte in eine andere Sprache übersetzt
|
||||||
|
Dunkel gehoben: lichtarmer oder völlig lichtloser Zustand, in dem sich etwas befindet oder der etwas auszeichnet
|
||||||
|
bleiern aus Blei bestehend; hergestellt aus Blei
|
||||||
|
Blutzoll gehoben: Verluste bei bewaffneten Konflikten
|
||||||
|
Silberbart gehoben: Bart mit hellem, silbrig schimmerndem Grauton
|
||||||
|
Paradiesgarten Religion: Ort, an den selige Menschen nach dem Tod kommen
|
||||||
|
Angedenken veraltet: kleiner Gegenstand, der Erinnerungen an ein bestimmtes Ereignis oder eine bestimmte Person hervorrufen soll
|
||||||
|
zerscherben gehoben: in einzelne Teile (Scherben) zerbrechen (lassen)
|
||||||
|
Begebnis gehoben oder veraltet: etwas, was geschieht/passiert/sich begibt
|
||||||
|
Ruhestatt gehoben: Platz/Stelle der Bestattung
|
||||||
|
zwischenein gehoben, zeitlich: in unregelmäßigen zeitlichen Abständen (ein Geschehen kurz unterbrechend)
|
||||||
|
Aperçu gehoben: geistreiche Bemerkung
|
||||||
|
hinwegfegen Hilfsverb sein: schnell über etwas hinwegbewegen
|
||||||
|
gelüsten unpersönlich, gehoben oder scherzhaft: großes Verlangen nach etwas haben, Lust auf etwas verspüren
|
||||||
|
blutenden Herzens gehoben: so dass es jemandem Anstrengung, Sorgen bereitet, jemanden Überwindung kostet; mit Bedenken, Zweifel; sehr ungern; ganz bekümmert; im Widerspruch zu jemandes Gefühl
|
||||||
|
Ich-Sucht gehoben, veraltend: Haltung, die darauf aus ist, sich selbst immer zuerst zu berücksichtigen
|
||||||
|
nunmehrig österreichisch, sonst gehoben: jetzt stattfindend; sich auf die unmittelbare Gegenwart beziehend
|
||||||
|
herabwerfen transitiv; gehoben: von (dort) oben nach (hier) unten werfen (in Richtung zum Sprechenden)
|
||||||
|
kundgeben transitiv, gehoben: etwas öffentlich bekannt geben/machen, jemanden etwas wissen lassen
|
||||||
|
hinzuaddieren transitiv, gehoben: (etwas) zu einer bekannten Zahl rechnerisch hinzufügen
|
||||||
|
dartun gehoben: etwas erläuternd darlegen, etwas zum Ausdruck bringen
|
||||||
|
einbekennen österreichisch, sonst gehoben: eingestehen
|
||||||
|
Sinnesverwirrung gehoben: (vorübergehende) falsche Interpretation von Sinneseindrücken (zum Beispiel durch Drogenkonsum oder Krankheit)
|
||||||
|
Spross Botanik: junger Trieb an einer Pflanze, neu wachsender Teil einer Pflanze
|
||||||
|
auslesen etwas aus einer Menge desselben heraussuchen und entfernen (weil es unbrauchbar [geworden] ist)
|
||||||
|
behagen intransitiv; gehoben, unpersönlich, mit Dativ: ein wohliges, still vergnügliches Gefühl bereiten
|
||||||
|
Brüderpaar gehoben: zwei Brüder
|
||||||
|
Schimpf gehoben, meist Singular: Verletzung der Selbstachtung
|
||||||
|
sinnenfroh gehoben: mit Sinnenfreude; sich am sinnlichen Genuss erfreuend
|
||||||
|
abschreiten transitiv, Hilfsverb haben oder Hilfsverb sein: eine Strecke zur Kontrolle, prüfend oder feierlich entlanggehen
|
||||||
|
Otterngezücht gehoben abwertend übertragen: boshafte, verachtenswerte Menschen
|
||||||
|
Sprachmittler gehoben: Person, die Text aus einer anderen Sprache übersetzt
|
||||||
|
davonstieben intransitiv, gehoben, von einer Gruppe oder Menge: rasch, fluchtartig, sich (in verschiedene Richtungen) zerstreuend, weglaufen
|
||||||
|
Bildner veraltet: jemand, der eine Person oder Sache formt, ausbildet
|
||||||
|
Disput gehoben: Gespräch oder (seltener schriftlicher) Gedankenaustausch zwischen zwei oder mehr Personen, mit dem Ziel, die jeweilige Gegenseite von den eigenen Argumenten zu überzeugen
|
||||||
|
nächtlicherweile gehoben: während es Nacht ist, in der Nacht, bei Nacht
|
||||||
|
wehklagen gehoben: seelische Not äußern
|
||||||
|
Blütenschnee dichterisch oder gehoben; übertragen: Menge weißer/weißlicher Blüten (die wie Schnee langsam niederfallen, sich wie dieser auf dem Boden ausgebreitet haben, …)
|
||||||
|
Grundgütiger gehoben, meist als Ausruf: der einzige, allmächtige, unfehlbare Herr
|
||||||
|
obschon gehoben: einräumende Subjunktion
|
||||||
|
Band dasjenige, was verbindet oder verknüpft
|
||||||
|
prostituieren gehoben, pejorativ: etwas Anrüchiges, Unpassendes, Verwerfliches begehen oder jemandem vorwerfen
|
||||||
|
schlaftrunken gehoben: noch vom Schlaf benommen; durch Müdigkeit hervorgerufenes Gefühl, als sei man betrunken
|
||||||
|
unlängst gehoben: vor nicht allzu langer Zeit
|
||||||
|
allerorten gehoben oder veraltet: an jedem Ort, überall
|
||||||
|
abschweifen gehoben: von einer Richtung abweichen, die Richtung ändern
|
||||||
|
Berufsethos gehoben: moralisches Bewusstsein, verantwortungsbewusste Einstellung einer Berufsgruppe hinsichtlich der einzuhaltenden Pflichten, Normen bei der Ausübung des Berufs
|
||||||
|
zerrinnen intransitiv, gehoben: flüssig werden und zerfließen
|
||||||
|
Wegscheide gehoben: Stelle, an der sich ein Weg gabelt
|
||||||
|
einherschreiten gehoben: vor jemandes Augen eher langsam und gemessenen Schrittes umhergehen oder vorbeigehen
|
||||||
|
traun gehoben veraltet: in der Tat, wirklich, fürwahr, wahrlich
|
||||||
|
Fels kein Plural: zusammenhängende Masse festen Gesteins (Singular 1)
|
||||||
|
hintanstehen intransitiv; gehoben: sich zugunsten von jemand/etwas anderem zurückhalten; zugunsten von jemand/etwas anderem auf jemanden/etwas verzichten; zugunsten von jemand/etwas anderem unberücksichtigt, vernachlässigt werden
|
||||||
|
kundtun gehoben, veraltend: etwas anderen mitteilen
|
||||||
|
unveräußerlich so, dass es nicht beseitigt/aberkannt werden darf/kann
|
||||||
|
ichsüchtig gehoben: übermäßig auf die eigene Person fixiert, nur zum eigenen Vorteil handelnd
|
||||||
|
benehmen reflexiv: sich verhalten, sich auf bestimmte Weise in der Umwelt zeigen
|
||||||
|
Plazet gehoben: Erklärung des Einverständnisses
|
||||||
|
durchschneiden transitiv, gehoben: einen Raum oder eine Fläche in einer geraden Linie durchqueren
|
||||||
|
verwinden gehoben: psychisch über etwas hinwegkommen
|
||||||
|
innewohnen intransitiv, gehoben, mit Dativ: fester Bestandteil sein von etwas; grundsätzlich dazugehören; typische Charakteristik von etwas sein
|
||||||
|
vulgo bildungssprachlich: im Allgemeinen/auch unter der Bezeichnung x geläufig (oft mit abwertendem, drastischem Ton)
|
||||||
|
Disputantin gehoben: weibliche Person, die sich an einem Disput beteiligt
|
||||||
|
herüberbemühen transitiv, gehoben: jemanden darum ersuchen, die Mühe auf sich zu nehmen, um aus einiger Entfernung zum Sprecher hin zu kommen
|
||||||
|
erharren transitiv, selten, gehoben: auf etwas Ersehntes warten
|
||||||
|
süffisant gehoben: selbstgefällig, genüsslich überlegene Distanz zeigend, ein Gefühl von meist geistiger Überlegenheit zur Schau tragend, spöttisch-überheblich
|
||||||
|
Maiennacht dichterisch, gehoben: Nacht im Mai
|
||||||
|
Jüngling gehoben: junger, fast erwachsener Mann
|
||||||
|
hinfallen intransitiv: (mit Wucht) zu Boden, auf die Erde fallen
|
||||||
|
mondbeschienen dichterisch, gehoben: im Mondschein befindlich
|
||||||
|
woselbst veraltend, gehoben: dort an diesem Ort, an dieser Stelle
|
||||||
|
einbegreifen gehoben: auch beinhalten, dazunehmen
|
||||||
|
jüngstens gehoben oder veraltet: vor Kurzem, vor nicht langer Zeit
|
||||||
|
wegleugnen transitiv; umgangssprachlich: (etwas, eine Sache) durch Leugnen bereinigen, endgültig beseitigen
|
||||||
|
Aufschwung allgemein, gehoben: mehr Schwung, Auftrieb, Fortschritt
|
||||||
|
Erlesenheit gehoben, ohne Plural: außerordentlich gute, exquisite Qualität
|
||||||
|
preiswürdig gehoben: lobenswert, hervorragend • Diese Bedeutungsangabe sollte ergänzt werden.
|
||||||
|
Ferne große räumliche Distanz
|
||||||
|
Zauberbann dichterisch und gehoben, auch übertragen: auf jemanden oder etwas Einfluss nehmende, unwiderstehliche Kraft eines Zaubers
|
||||||
|
dahingehen intransitiv; gehoben: jemanden, etwas gehend passieren, sich an jemanden, etwas gehend vorbeibewegen; (ohne erkennbares, festes Ziel) von einer Stelle zu einer anderen gehen
|
||||||
|
davongehen sich zu Fuß von einem bestimmten Ort/vom Sprechenden entfernen
|
||||||
|
Flucht rasches Entfliehen von einer Sache, die Gefahr oder Unglück verheißt
|
||||||
|
zwiefältig gehoben, veraltend: zweimal so oft oder so viel
|
||||||
|
untersagen transitiv, gehoben: ein Verbot aussprechen
|
||||||
|
Tonkünstler gehoben, Musik: Künstler, der musikalische Werke schafft
|
||||||
|
Selbst gehoben: die eigene Person in ihrer Gesamtheit
|
||||||
|
gotikeit österreichisch, gehoben, veraltet, zur Beteuerung, parenthetisch verwendet: gewissermaßen, sozusagen, gleichsam, wirklich, bei Gott, will sagen
|
||||||
|
Menetekel gehoben: (geheimnisvolles) warnendes Vorzeichen eines drohenden Unheils, ernstes Warnzeichen
|
||||||
|
bewegen die Lage oder Stellung von etwas im Raum verändern; etwas von einem Ort zum anderen schaffen
|
||||||
|
Placet gehoben: Erklärung des Einverständnisses
|
||||||
|
Morgentoilette gehoben, kein Plural: Körperreinigung, Körperpflege und Ankleiden am Morgen
|
||||||
|
Diskutantin gehoben: weibliche Person, die sich an einer Diskussion beteiligt
|
||||||
|
schöpfen etwas aus einer großen Menge Flüssigkeit entnehmen
|
||||||
|
zurückgeleiten transitiv, gehoben: jemanden oder etwas wieder dorthin geleiten, wo er/sie/es zuvor hergekommen ist
|
||||||
|
gesunden nach einer Krankheit wieder gesund werden
|
||||||
|
abhold gehoben: abgeneigt, ungünstig
|
||||||
|
anheben ein wenig in die Höhe heben
|
||||||
|
soupieren intransitiv, gehoben: festlich/in großem Rahmen/mit Gästen zu Abend essen
|
||||||
|
leidlich gehoben: gerade noch ausreichend, akzeptabel
|
||||||
|
Bedenken tragen gehoben: noch unentschlossen sein, zagen, zaudern; Vorbehalte (gegen etwas) haben
|
||||||
|
geifern intransitiv: Speichel aus dem Mund fließen lassen
|
||||||
|
widerfahren gehoben; intransitiv, unpersönlich, mit Dativ: (jemandem) geschehen (besonders etwas Unausweichliches, scheinbar vom Schicksal Bestimmtes)
|
||||||
|
Verblichene gehoben: weibliche Person, die gestorben ist
|
||||||
|
zupasskommen gehoben: für jemanden gerade recht kommen, jemandem äußerst gelegen kommen, jemandem sehr willkommen sein
|
||||||
|
@@ -0,0 +1 @@
|
|||||||
|
Wort Definition Sprache Zeitstempel
|
||||||
|
@@ -0,0 +1,101 @@
|
|||||||
|
Wort Definition Sprache Zeitstempel
|
||||||
|
abbummeln eine zuvor außerhalb der regulären/vereinbarten Arbeitszeiten geleistete Arbeit (Mehrarbeit) durch Freizeit anstatt Bezahlung ausgleichen de 2025-09-29 18:26:54
|
||||||
|
abschussbereit vorbereitet sein, zu schießen oder etwas abzuschießen de 2025-09-29 18:26:27
|
||||||
|
Aalfischerei das gewerbliche, wirtschaftliche Betreiben des Aalfangs de 2025-09-29 18:27:02
|
||||||
|
abendlich zum Abend gehörend; am Abend, abends stattfindend de 2025-09-29 18:26:37
|
||||||
|
abhandenkommen verloren gehen, nicht mehr auffindbar sein de 2025-09-29 18:26:51
|
||||||
|
abfieseln mit den Zähnen, mit den Fingern oder mit einem Messer Fleisch (gründlich) von den Knochen lösen de 2025-09-29 18:26:27
|
||||||
|
A-Saite musik]]:'' Saite, die auf den Ton A oder a gestimmt ist de 2025-09-29 18:26:26
|
||||||
|
abgelegen weit weg und schlecht zugänglich de 2025-09-29 18:26:06
|
||||||
|
abhorreszieren abscheu gegen etwas oder jemanden empfinden de 2025-09-29 18:26:10
|
||||||
|
abackern etwas durch Bearbeitung der Erde absondern und somit aus dem Besitz einer anderen Person entziehen de 2025-09-29 18:26:07
|
||||||
|
abdikativ sich auf eine Abdikation beziehend oder sie bewirkend de 2025-09-29 18:27:03
|
||||||
|
ablandig geographie]], Hydrologie, Meteorologie, Klimatologie:'' hinsichtlich auf Luftströmungen und Meeresströmungen bezogen: vom Lande wegführend de 2025-09-29 18:26:43
|
||||||
|
abendfüllend den gesamten oder zumindest einen Großteil des Abends andauernd de 2025-09-29 18:26:20
|
||||||
|
abkarten eine heimliche Vereinbarung treffen, etwas heimlich zu jemandes Nachteil aushandeln de 2025-09-29 18:27:08
|
||||||
|
abdüsen sich schnell von einem Ort entfernen, meist mithilfe eines Fortbewegungsmittels de 2025-09-29 18:26:13
|
||||||
|
a-Schwa vokal, der zwischen einem [a] und einem [ə] (Schwa) liegt und im Deutschen nur unbetont vorkommt de 2025-09-29 18:26:46
|
||||||
|
abchecken überprüfen]], kontrollieren de 2025-09-29 18:27:09
|
||||||
|
abhörsicher von Räumen oder Geräten:'' vor Abhöraktionen geschützt; so beschaffen, dass es nicht abgehört werden kann de 2025-09-29 18:26:24
|
||||||
|
abhauen etwas abschlagen, abtrennen (starke und schwache Konjugation: hieb/haute ab und abgehauen/abgehaut) de 2025-09-29 18:27:07
|
||||||
|
abbestellen etwas abbestellen:'' eine Bestellung rückgängig machen, etwas kündigen de 2025-09-29 18:26:11
|
||||||
|
abkrüppeln sterben, abkratzen, verrecken de 2025-09-29 18:26:45
|
||||||
|
abhinfallen veraltet; bairisch, südalemannisch:'' vom Sprecher weg fallen de 2025-09-29 18:26:36
|
||||||
|
abbehalten (eine Kopfbedeckung) nach dem Abnehmen (vorerst) nicht erneut aufsetzen de 2025-09-29 18:26:44
|
||||||
|
abgemergelt in schlechter körperlicher Verfassung, durch Arbeit erschöpft de 2025-09-29 18:26:34
|
||||||
|
abflachen flach(er) machen de 2025-09-29 18:26:28
|
||||||
|
aboral vom Mund weg gelegen de 2025-09-29 18:26:32
|
||||||
|
abdreschen stroh ausdreschen, um das Getreide zu erhalten; das Getreide aus dem Stroh dreschen de 2025-09-29 18:26:50
|
||||||
|
abbussen süddeutsch, österreichisch, umgangssprachlich:'' über und über mit Küssen bedecken de 2025-09-29 18:26:49
|
||||||
|
abern einwände anbringen, erheben de 2025-09-29 18:27:06
|
||||||
|
aasfräßig sich von Aas ernährend de 2025-09-29 18:26:40
|
||||||
|
Aalstechen art des Aalfanges mittels Stechen nach den Aalen mit Aalspeeren de 2025-09-29 18:26:42
|
||||||
|
Aalmöwe im Ostatlantik und den nordeuropäischen Küsten lebender Barschartiger, de 2025-09-29 18:27:10
|
||||||
|
abenteuerhungrig übertragen:'' hungrig auf Abenteuer; ein Abenteuer vermissend, erwartend, sich nach einem Abenteuer sehnend de 2025-09-29 18:26:30
|
||||||
|
Aalbaum veraltet]], Botanik:'' Geißblattgewächs mit eierförmigen Blättern, gelben Blüten und roten, für den Menschen ungenießbaren Früchten de 2025-09-29 18:27:02
|
||||||
|
abendländisch das Abendland betreffend, zu ihm gehörend, charakteristisch für das Abendland de 2025-09-29 18:26:47
|
||||||
|
abfretten sich abmühen de 2025-09-29 18:27:04
|
||||||
|
abgelehnt zurückgewiesen, nicht genehmigt de 2025-09-29 18:26:26
|
||||||
|
abfohlen ein Jungtier (Fohlen) zur Welt bringen de 2025-09-29 18:26:09
|
||||||
|
abgrätschen mit gestreckten und gespreizten Beinen von einem Turngerät springen, Abgang von einem Gerät mit einer Grätsche de 2025-09-29 18:27:05
|
||||||
|
abfieren etwas am Tau herunterlassen de 2025-09-29 18:26:34
|
||||||
|
abkömmlich nicht zwingend benötigt; verzichtbar, unnotwendig, disponibel de 2025-09-29 18:27:06
|
||||||
|
abrufbar so, dass man Zugriff darauf hat; so, dass einem etwas zur Verfügung steht de 2025-09-29 18:26:32
|
||||||
|
abgekämpft erschöpft von sehr großer Anstrengung de 2025-09-29 18:26:30
|
||||||
|
abgerissen in einem schlechten Zustand befindlich, zerlumpt, mitgenommen de 2025-09-29 18:27:00
|
||||||
|
abbimsen zur Glättung/Reinigung mit einem Bimsstein abreiben de 2025-09-29 18:26:58
|
||||||
|
abberufen jemanden von seinem Amt oder seiner Funktion entheben de 2025-09-29 18:26:55
|
||||||
|
abbitten jemanden für etwas um Verzeihung bitten de 2025-09-29 18:26:29
|
||||||
|
Aakerbeere mehrjährige Pflanze mit leuchtend roten bis purpurnen Blüten und roten, essbaren Früchten, die im Norden Eurasiens und in Nordamerika vorkommt () de 2025-09-29 18:26:41
|
||||||
|
abfetzen etwas (in Fetzen) nach unten reißen de 2025-09-29 18:27:08
|
||||||
|
abalienieren entäußern, veräußern de 2025-09-29 18:26:38
|
||||||
|
abgegrenzt voneinander getrennt de 2025-09-29 18:26:57
|
||||||
|
Aalmolch familie]] der Schwanzlurche, die aalförmig gestreckt sind de 2025-09-29 18:26:52
|
||||||
|
abjustieren abgleichen]] de 2025-09-29 18:26:16
|
||||||
|
Aalsuppe suppe aus Aal de 2025-09-29 18:26:07
|
||||||
|
abbellen (zum Beispiel eine Rede) laut, aber unverständlich, mit heiserer Stimme vortragen de 2025-09-29 18:26:50
|
||||||
|
abjudizieren jemandem etwas (gerichtlich) aberkennen, absprechen de 2025-09-29 18:26:48
|
||||||
|
Aalenium geologie]]:'' geologisch unterste Stufe des Mittleren Jura Braunjura de 2025-09-29 18:26:59
|
||||||
|
Aalspeer gerät]], das einem Speer ähnelt, jedoch über mehrere Zinken verfügt und zum Aalstechen benutzt wird de 2025-09-29 18:26:54
|
||||||
|
aasfressend aas fressend, sich von toten Organismen ernährend de 2025-09-29 18:26:29
|
||||||
|
abblitzen von jemandem zurückgewiesen werden, jemandem gegenüber keine Chance zur Kommunikation haben de 2025-09-29 18:26:22
|
||||||
|
abenteuersüchtig mit einem übersteigerten Verlangen nach Abenteuern de 2025-09-29 18:26:25
|
||||||
|
aargauisch den Kanton Aargau oder dessen Bewohner betreffend, aus dem Aargau kommend de 2025-09-29 18:26:20
|
||||||
|
Aalfischer person, die Aale fängt de 2025-09-29 18:26:42
|
||||||
|
abschlägig ein Ansinnen verweigernd de 2025-09-29 18:27:01
|
||||||
|
abfechten jemandem etwas durch eine Kampfhandlung abgewinnen de 2025-09-29 18:26:18
|
||||||
|
abgasentgiftet so beschaffen, dass keine giftigen Abgase vorhanden sind de 2025-09-29 18:26:45
|
||||||
|
abdecken eine Decke entfernen de 2025-09-29 18:26:19
|
||||||
|
abfiedeln etwas mit einem stumpfen Schneidwerkzeug schneidend entfernen de 2025-09-29 18:26:12
|
||||||
|
abnorm von der Norm oder dem Üblichen (Normalen) abweichend de 2025-09-29 18:26:21
|
||||||
|
abblassen farblich]] weniger intensiv werden, blass werden de 2025-09-29 18:26:53
|
||||||
|
Aap kleines Stagsegel auf Segelschiffen de 2025-09-29 18:26:35
|
||||||
|
abgefuckt jargon]], derb, vulgär:'' in schlechtem, üblem Zustand befindlich de 2025-09-29 18:26:08
|
||||||
|
abdorren trocken werden (und dadurch absterben) de 2025-09-29 18:26:08
|
||||||
|
abgelenkt geistig]] abwesend, nicht auf eine Sache konzentriert de 2025-09-29 18:26:33
|
||||||
|
abduschen etwas oder sich durch Duschen reinigen de 2025-09-29 18:26:12
|
||||||
|
Aalrutte knochenfisch aus der Familie der Quappen de 2025-09-29 18:26:24
|
||||||
|
abfangen jemanden oder etwas auf seinem Weg aufhalten de 2025-09-29 18:27:03
|
||||||
|
abteilungsübergreifend mehr als eine Abteilung betreffend; mehrere Abteilungen umfassend de 2025-09-29 18:26:36
|
||||||
|
absichtlich die Auswirkung seiner Handlung wollend de 2025-09-29 18:26:23
|
||||||
|
abducken den Schlag eines Gegners durch Ducken ins Leere gehen lassen de 2025-09-29 18:26:39
|
||||||
|
aalähnlich wie ein Aal, an einen Aal erinnernd de 2025-09-29 18:26:48
|
||||||
|
abbaken den Fahrweg mit Baken oder eingeschlagenen Pfählen markieren de 2025-09-29 18:26:59
|
||||||
|
abfallarm so beschaffen, dass wenig Abfall entsteht de 2025-09-29 18:26:58
|
||||||
|
abhandeln mit jemandem über etwas verhandeln, sprechen, mit dem Ziel, dass man von ihm etwas bekommt de 2025-09-29 18:27:05
|
||||||
|
abbalzen die Balz beenden de 2025-09-29 18:26:33
|
||||||
|
abmarschbereit zum Abmarsch bereit de 2025-09-29 18:26:13
|
||||||
|
abkanzeln mit harschen Worten, von oben herab, kritisieren de 2025-09-29 18:26:31
|
||||||
|
abbreiten mit einem Breithammer (in bestimmtem Maße) breit schlagen de 2025-09-29 18:26:55
|
||||||
|
abbrüchig so beschaffen, dass es leicht abbricht de 2025-09-29 18:26:17
|
||||||
|
abdingbar durch andere Vereinbarungen abänderbar de 2025-09-29 18:26:57
|
||||||
|
abkommen veraltet]]:'' von (etwas, jemandem) stammen, auf (etwas, jemanden) zurückgehen de 2025-09-29 18:26:15
|
||||||
|
abborgen jemandem etwas abborgen:'' mit dem Versprechen der baldigen Rückgabe vorübergehend in seinen Besitz nehmen de 2025-09-29 18:27:02
|
||||||
|
abflusslos keinen Abfluss habend de 2025-09-29 18:26:59
|
||||||
|
abgasarm nur eine relativ kleine Menge an (schädlichen) Abgasen ausstoßend de 2025-09-29 18:26:18
|
||||||
|
abgaunern jemandem etwas durch List, Betrug, Täuschung widerrechtlich wegnehmen de 2025-09-29 18:27:05
|
||||||
|
abkindern einen Ehekredit durch das Zeugen von Kindern tilgen de 2025-09-29 18:26:39
|
||||||
|
abermalig nochmalig, wiederholt de 2025-09-29 18:26:17
|
||||||
|
Aalstreif zoologie]]:'' schwarzer Streifen längs auf dem Rücken eines Tieres, meist eines Pferdes de 2025-09-29 18:26:47
|
||||||
|
abachen sich bei etwas bis zur Erschöpfung anstrengen und ächzen de 2025-09-29 18:26:56
|
||||||
|
abasinisch auf das Volk der Abasiner/Abasinen bezogen, von ihm stammend, zu ihm gehörend de 2025-09-29 18:26:25
|
||||||
|
@@ -0,0 +1,489 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class WiktionaryClient {
|
||||||
|
private $lang;
|
||||||
|
private $api_url;
|
||||||
|
private $debug;
|
||||||
|
private $is_cli;
|
||||||
|
private $category_cache = [];
|
||||||
|
|
||||||
|
public function __construct($lang = 'en', $debug = false, $is_cli = true) {
|
||||||
|
$this->lang = $lang;
|
||||||
|
$this->api_url = "https://{$lang}.wiktionary.org/w/api.php";
|
||||||
|
$this->debug = $debug;
|
||||||
|
$this->is_cli = $is_cli;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function debug_print($msg) {
|
||||||
|
if ($this->debug) {
|
||||||
|
echo "DEBUG: $msg\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function progress_print($msg) {
|
||||||
|
if ($this->is_cli) {
|
||||||
|
echo "\r$msg";
|
||||||
|
} else {
|
||||||
|
echo "$msg<br>";
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_category_members($category) {
|
||||||
|
if (isset($this->category_cache[$category])) {
|
||||||
|
$this->debug_print("Cache-Hit für Kategorie '$category': " . count($this->category_cache[$category]) . " Member");
|
||||||
|
return $this->category_cache[$category];
|
||||||
|
}
|
||||||
|
|
||||||
|
$members = [];
|
||||||
|
$params = [
|
||||||
|
'action' => 'query',
|
||||||
|
'list' => 'categorymembers',
|
||||||
|
'cmtitle' => "Category:$category",
|
||||||
|
'cmtype' => 'page',
|
||||||
|
'cmlimit' => '500',
|
||||||
|
'format' => 'json'
|
||||||
|
];
|
||||||
|
$this->debug_print("Starte Abruf für Kategorie '$category' mit URL: {$this->api_url} und Params: " . http_build_query($params));
|
||||||
|
$cmcontinue = '';
|
||||||
|
$page = 1;
|
||||||
|
$total_members = 0;
|
||||||
|
while (true) {
|
||||||
|
$query = http_build_query($params) . ($cmcontinue ? "&cmcontinue=$cmcontinue" : '');
|
||||||
|
$ch = curl_init("{$this->api_url}?$query");
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_USERAGENT, 'GrokScript/1.0');
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$curl_error = curl_error($ch);
|
||||||
|
$status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
if ($curl_error) {
|
||||||
|
echo "Fehler beim API-Aufruf für $category: $curl_error\n";
|
||||||
|
$this->debug_print("Curl Error Details: $curl_error");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$this->debug_print("Response Status: $status_code, Content-Length: " . strlen($response));
|
||||||
|
if ($status_code != 200) {
|
||||||
|
echo "Fehler beim Abrufen von $category: $status_code\n";
|
||||||
|
$this->debug_print("Response Text: " . substr($response, 0, 500) . "...");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
$this->debug_print("Data Keys: " . implode(', ', array_keys($data)));
|
||||||
|
$new_members = [];
|
||||||
|
foreach ($data['query']['categorymembers'] ?? [] as $member) {
|
||||||
|
$new_members[] = $member['title'];
|
||||||
|
}
|
||||||
|
$members = array_merge($members, $new_members);
|
||||||
|
$total_members += count($new_members);
|
||||||
|
$this->progress_print("Kategorie '$category' Paging: Seite $page abgerufen (" . count($new_members) . " neue, total $total_members)");
|
||||||
|
if (isset($data['continue']['cmcontinue'])) {
|
||||||
|
$cmcontinue = $data['continue']['cmcontinue'];
|
||||||
|
$this->debug_print("Paginierung fortsetzen mit cmcontinue: $cmcontinue");
|
||||||
|
$page++;
|
||||||
|
} else {
|
||||||
|
if ($this->is_cli) echo "\n"; else echo "<br>";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sleep(0.5); // Pause
|
||||||
|
}
|
||||||
|
$this->category_cache[$category] = $members;
|
||||||
|
$this->debug_print("Abgerufene Member für '$category' (cached): " . count($members));
|
||||||
|
return $members;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function extract_definition_and_variant($extract, $preferred_variant = null) {
|
||||||
|
$lines = explode("\n", $extract);
|
||||||
|
$definition = 'Keine Definition gefunden';
|
||||||
|
$variant = '';
|
||||||
|
$in_bedeutungen = false;
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (strpos($line, 'Bedeutungen:') !== false) {
|
||||||
|
$in_bedeutungen = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($in_bedeutungen) {
|
||||||
|
if (strpos($line, '[1] ') === 0) {
|
||||||
|
$definition = trim(substr($line, 4));
|
||||||
|
break;
|
||||||
|
} elseif (strpos($line, '[1]') === 0) {
|
||||||
|
$definition = trim(substr($line, strpos($line, '[1]') + 3));
|
||||||
|
break;
|
||||||
|
} elseif (preg_match('/^\[\d+\]/', $line)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Alternative Schreibweise suchen
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (strpos($line, 'Alternative Schreibweisen:') !== false || strpos($line, 'Nebenformen:') !== false) {
|
||||||
|
// Split nach Labels wie "Schweizerische Schreibweise:"
|
||||||
|
$parts = preg_split('/\w+ische Schreibweise:/i', $line);
|
||||||
|
foreach ($parts as $index => $part) {
|
||||||
|
if ($index > 0) { // Nach dem ersten Label
|
||||||
|
preg_match_all('/[\w\-]+/', $part, $matches);
|
||||||
|
if (!empty($matches[0])) {
|
||||||
|
if ($preferred_variant && stripos($line, $preferred_variant) !== false) {
|
||||||
|
$variant = $matches[0][0];
|
||||||
|
break 2; // Beende äußere Schleife
|
||||||
|
} elseif (!$preferred_variant) {
|
||||||
|
$variant = $matches[0][0];
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [$definition, $variant];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_definitions_batch($words) {
|
||||||
|
$definitions = [];
|
||||||
|
$variants = [];
|
||||||
|
foreach ($words as $idx => $word) {
|
||||||
|
$params = [
|
||||||
|
'action' => 'query',
|
||||||
|
'prop' => 'extracts',
|
||||||
|
'titles' => $word,
|
||||||
|
'explaintext' => '1',
|
||||||
|
'exsectionformat' => 'plain',
|
||||||
|
'format' => 'json'
|
||||||
|
];
|
||||||
|
$query = http_build_query($params);
|
||||||
|
$this->debug_print("Starte Definitionsabruf für Wort " . ($idx + 1) . "/" . count($words) . ": '$word' mit URL: {$this->api_url} und Params: $query");
|
||||||
|
$ch = curl_init("{$this->api_url}?$query");
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_USERAGENT, 'GrokScript/1.0');
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$curl_error = curl_error($ch);
|
||||||
|
$status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
if ($curl_error) {
|
||||||
|
echo "Fehler beim Definitionsabruf für '$word': $curl_error\n";
|
||||||
|
$this->debug_print("Curl Error Details: $curl_error");
|
||||||
|
$definitions[$word] = 'Keine Definition gefunden';
|
||||||
|
$variants[$word] = '';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$this->debug_print("Response Status: $status_code, Content-Length: " . strlen($response));
|
||||||
|
if ($status_code != 200) {
|
||||||
|
echo "Fehler beim Abrufen von Definition für '$word': $status_code\n";
|
||||||
|
$this->debug_print("Response Text: " . substr($response, 0, 500) . "...");
|
||||||
|
$definitions[$word] = 'Keine Definition gefunden';
|
||||||
|
$variants[$word] = '';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
$this->debug_print("Data Keys: " . implode(', ', array_keys($data)));
|
||||||
|
$page = reset($data['query']['pages'] ?? []) ?? [];
|
||||||
|
$extract = $page['extract'] ?? '';
|
||||||
|
list($defn, $var) = $this->extract_definition_and_variant($extract, $this->preferred_variant);
|
||||||
|
$definitions[$word] = $defn;
|
||||||
|
$variants[$word] = $var;
|
||||||
|
$this->progress_print("Definitionen abrufen: " . ($idx + 1) . "/" . count($words) . " abgeschlossen ($word)");
|
||||||
|
sleep(0.5); // Pause pro Request zur Rate-Limiting-Vermeidung
|
||||||
|
}
|
||||||
|
if ($this->is_cli) echo "\n"; else echo "<br>";
|
||||||
|
$this->debug_print("Abgerufene Definitionen: " . count($definitions));
|
||||||
|
return [$definitions, $variants];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function list_all_categories($filter_str = null) {
|
||||||
|
$categories = [];
|
||||||
|
$params = [
|
||||||
|
'action' => 'query',
|
||||||
|
'list' => 'allcategories',
|
||||||
|
'aclimit' => '500',
|
||||||
|
'format' => 'json'
|
||||||
|
];
|
||||||
|
$this->debug_print("Starte Abruf aller Kategorien mit URL: {$this->api_url} und Params: " . http_build_query($params));
|
||||||
|
$accontinue = '';
|
||||||
|
$page = 1;
|
||||||
|
$total_categories = 0;
|
||||||
|
while (true) {
|
||||||
|
$query = http_build_query($params) . ($accontinue ? "&accontinue=$accontinue" : '');
|
||||||
|
$ch = curl_init("{$this->api_url}?$query");
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_USERAGENT, 'GrokScript/1.0');
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$curl_error = curl_error($ch);
|
||||||
|
$status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
if ($curl_error) {
|
||||||
|
echo "Fehler beim API-Aufruf für Kategorien: $curl_error\n";
|
||||||
|
$this->debug_print("Curl Error Details: $curl_error");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$this->debug_print("Response Status: $status_code, Content-Length: " . strlen($response));
|
||||||
|
if ($status_code != 200) {
|
||||||
|
echo "Fehler beim Abrufen der Kategorien: $status_code\n";
|
||||||
|
$this->debug_print("Response Text: " . substr($response, 0, 500) . "...");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
$this->debug_print("Data Keys: " . implode(', ', array_keys($data)));
|
||||||
|
$new_categories = [];
|
||||||
|
foreach ($data['query']['allcategories'] ?? [] as $cat) {
|
||||||
|
$new_categories[] = $cat['*'];
|
||||||
|
}
|
||||||
|
$categories = array_merge($categories, $new_categories);
|
||||||
|
$total_categories += count($new_categories);
|
||||||
|
$this->progress_print("Kategorien Paging: Seite $page abgerufen (" . count($new_categories) . " neue, total $total_categories)");
|
||||||
|
if (isset($data['continue']['accontinue'])) {
|
||||||
|
$accontinue = $data['continue']['accontinue'];
|
||||||
|
$this->debug_print("Paginierung fortsetzen mit accontinue: $accontinue");
|
||||||
|
$page++;
|
||||||
|
} else {
|
||||||
|
if ($this->is_cli) echo "\n"; else echo "<br>";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sleep(0.5); // Pause
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($filter_str) {
|
||||||
|
$filter_lower = strtolower($filter_str);
|
||||||
|
$categories = array_filter($categories, function($cat) use ($filter_lower) {
|
||||||
|
return strpos(strtolower($cat), $filter_lower) !== false;
|
||||||
|
});
|
||||||
|
$this->debug_print("Gefiltert nach '$filter_str': " . count($categories) . " Kategorien gefunden");
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Vorhandene Kategorien" . ($filter_str ? " (gefiltert nach '$filter_str')" : "") . ":\n";
|
||||||
|
sort($categories);
|
||||||
|
foreach ($categories as $cat) {
|
||||||
|
echo "$cat\n";
|
||||||
|
}
|
||||||
|
$this->debug_print("Gesamte Kategorien: " . count($categories));
|
||||||
|
return $categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function parse_include_logic($include_str) {
|
||||||
|
if (empty($include_str)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$groups = explode(',', $include_str);
|
||||||
|
$parsed_groups = [];
|
||||||
|
foreach ($groups as $group) {
|
||||||
|
$cats = array_unique(explode('+', $group)); // Unique in Gruppe
|
||||||
|
$parsed_groups[] = array_filter(array_map('trim', $cats));
|
||||||
|
}
|
||||||
|
return array_filter($parsed_groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_words_from_logic($include_groups) {
|
||||||
|
$all_words = [];
|
||||||
|
foreach ($include_groups as $group) {
|
||||||
|
if (empty($group)) continue;
|
||||||
|
$group_members = [];
|
||||||
|
foreach ($group as $cat) {
|
||||||
|
$group_members[] = $this->get_category_members($cat);
|
||||||
|
}
|
||||||
|
if (!empty($group_members)) {
|
||||||
|
$intersection = call_user_func_array('array_intersect', $group_members);
|
||||||
|
$all_words = array_merge($all_words, $intersection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array_unique($all_words);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generate_wordlist($include, $exclude, $num_words, $output_file, $preferred_variant = null, $headers_flag = false, $format = 'csv') {
|
||||||
|
$this->preferred_variant = $preferred_variant;
|
||||||
|
$include_groups = $this->parse_include_logic($include);
|
||||||
|
if (empty($include_groups)) {
|
||||||
|
echo "Fehler: --include muss angegeben werden, da 'alle' nicht unterstützt wird.\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$all_words = $this->get_words_from_logic($include_groups);
|
||||||
|
$this->debug_print("Include-Wörter nach Logik: " . count($all_words));
|
||||||
|
|
||||||
|
$exclude_cats = array_unique(array_map('trim', explode(',', $exclude)));
|
||||||
|
$exclude_words = [];
|
||||||
|
foreach ($exclude_cats as $cat) {
|
||||||
|
if (empty($cat)) continue;
|
||||||
|
echo "Abrufen von Kategorie (exclude): $cat\n";
|
||||||
|
$members = $this->get_category_members($cat);
|
||||||
|
$exclude_words = array_merge($exclude_words, $members);
|
||||||
|
}
|
||||||
|
$this->debug_print("Exclude-Wörter: " . count($exclude_words));
|
||||||
|
|
||||||
|
$all_words = array_diff($all_words, $exclude_words);
|
||||||
|
$this->debug_print("Finale Wörter nach Exclude: " . count($all_words));
|
||||||
|
|
||||||
|
if (empty($all_words)) {
|
||||||
|
echo "Keine Wörter gefunden.\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
shuffle($all_words); // Hohe Zufälligkeit
|
||||||
|
$selected_words = array_slice($all_words, 0, $num_words);
|
||||||
|
$this->debug_print("Ausgewählte Wörter: " . count($selected_words));
|
||||||
|
|
||||||
|
echo "Abrufen von Definitionen für " . count($selected_words) . " Wörter...\n";
|
||||||
|
list($definitions, $variants) = $this->get_definitions_batch($selected_words);
|
||||||
|
|
||||||
|
if ($format === 'html') {
|
||||||
|
$output = "<!DOCTYPE html>\n<html lang='en'>\n<head>\n<meta charset='UTF-8'>\n";
|
||||||
|
$output .= "<title>Wiktionary Wortliste</title>\n";
|
||||||
|
$output .= "<link href='https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css' rel='stylesheet'>\n";
|
||||||
|
$output .= "</head>\n<body class='bg-gray-100 p-4'>\n";
|
||||||
|
$output .= "<div class='container mx-auto'>\n";
|
||||||
|
$output .= "<h1 class='text-2xl font-bold mb-4'>Wiktionary Wortliste ({$this->lang})</h1>\n";
|
||||||
|
$output .= "<table class='min-w-full bg-white border border-gray-300'>\n";
|
||||||
|
if ($headers_flag) {
|
||||||
|
$output .= "<thead><tr class='bg-gray-200'><th class='py-2 px-4 border'>Begriff</th><th class='py-2 px-4 border'>Erste Bedeutung</th></tr></thead>\n";
|
||||||
|
}
|
||||||
|
$output .= "<tbody>\n";
|
||||||
|
foreach ($selected_words as $word) {
|
||||||
|
$var = $variants[$word] ?? '';
|
||||||
|
$begriff = $var ? htmlspecialchars($var . ', ' . $word) : htmlspecialchars($word);
|
||||||
|
$defn = htmlspecialchars($definitions[$word] ?? 'Keine Definition gefunden');
|
||||||
|
$output .= "<tr><td class='py-2 px-4 border'>$begriff</td><td class='py-2 px-4 border'>$defn</td></tr>\n";
|
||||||
|
}
|
||||||
|
$output .= "</tbody>\n</table>\n</div>\n</body>\n</html>";
|
||||||
|
if ($this->is_cli) {
|
||||||
|
file_put_contents($output_file, $output);
|
||||||
|
echo "HTML-Tabelle gespeichert in $output_file\n";
|
||||||
|
} else {
|
||||||
|
header('Content-Type: text/html; charset=UTF-8');
|
||||||
|
echo $output;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$file = fopen($output_file, 'w');
|
||||||
|
if ($headers_flag) {
|
||||||
|
fputcsv($file, ['Begriff', 'Erste Bedeutung'], "\t");
|
||||||
|
}
|
||||||
|
foreach ($selected_words as $word) {
|
||||||
|
$var = $variants[$word] ?? '';
|
||||||
|
$begriff = $var ? ($var . ', ' . $word) : $word;
|
||||||
|
$defn = $definitions[$word] ?? 'Keine Definition gefunden';
|
||||||
|
fputcsv($file, [$begriff, $defn], "\t");
|
||||||
|
}
|
||||||
|
fclose($file);
|
||||||
|
echo "Tabelle gespeichert in $output_file\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function print_help() {
|
||||||
|
$help_text = <<<EOT
|
||||||
|
Wiktionary Wortliste Generator
|
||||||
|
|
||||||
|
Dieses Skript generiert eine Wortliste mit Begriffen und deren ersten Bedeutungen aus einer Wiktionary-Instanz
|
||||||
|
oder listet verfügbare Kategorien auf.
|
||||||
|
|
||||||
|
Verfügbare Argumente:
|
||||||
|
--lang, -l <Sprachcode> Sprachcode für die Wiktionary-Instanz (z.B. 'de' für de.wiktionary.org).
|
||||||
|
Default: 'en'. Erforderlich.
|
||||||
|
--include, -i <Logik> Logik für einzubeziehende Kategorien (z.B. 'A+B,C' für (A UND B) ODER C).
|
||||||
|
Erforderlich für Wortlisten (keine Includes führt zu Fehler), ignoriert bei --list-categories.
|
||||||
|
--exclude, -e <Kategorien> Komma-separierte Liste auszuschließender Kategorien (OR-Logik).
|
||||||
|
Optional, ignoriert bei --list-categories.
|
||||||
|
--preferred-variant <Name> Bevorzugte Schreibweise (z.B. 'Schweizerische' – extrahiert aus Excerpt und als erstes ausgegeben).
|
||||||
|
Optional.
|
||||||
|
--filter, -f <Text> Textfilter für Kategorien (Substring-Match, case-insensitive).
|
||||||
|
Optional, nur bei --list-categories relevant.
|
||||||
|
--format, -o <Format> Ausgabeformat: 'csv' (Standard) oder 'html' (Tabelle mit Tailwind CSS).
|
||||||
|
Optional.
|
||||||
|
--debug, -d Aktiviert ausführliche Debug-Ausgaben für API-Aufrufe und Prozesse.
|
||||||
|
Optional.
|
||||||
|
--headers, -r Aktiviert die Ausgabe der Spaltenüberschrift in der Ausgabe (default: deaktiviert).
|
||||||
|
Optional, nur für Wortlisten relevant.
|
||||||
|
--list-categories, -c Listet alle Kategorien der Wiktionary-Instanz (optional mit --filter).
|
||||||
|
Wenn gesetzt, werden andere Parameter (außer --lang, --filter, --debug) ignoriert.
|
||||||
|
--help, -h Zeigt diese Hilfe an und beendet das Skript.
|
||||||
|
num_words Anzahl der Wörter für die Wortliste (Integer).
|
||||||
|
Erforderlich für Wortlisten, ignoriert bei --list-categories.
|
||||||
|
output_file Ausgabedatei für die Wortliste (CSV oder HTML).
|
||||||
|
Erforderlich für Wortlisten, ignoriert bei --list-categories. Default: 'output.csv'.
|
||||||
|
|
||||||
|
Argumentkombinationen:
|
||||||
|
1. Wortliste generieren:
|
||||||
|
- Erforderlich: --lang, --include, num_words, output_file
|
||||||
|
- Optional: --exclude, --preferred-variant, --format, --debug, --headers
|
||||||
|
- Beispiel: php script.php --lang de --include "Substantiv (Deutsch)+Deutsch,Verb (Deutsch)" --exclude "Veraltete Substantive (Deutsch)" --preferred-variant "Schweizerische" --format html 10000 output.html --debug --headers
|
||||||
|
2. Kategorien auflisten:
|
||||||
|
- Erforderlich: --lang, --list-categories
|
||||||
|
- Optional: --filter, --debug
|
||||||
|
- Beispiel: php script.php --lang de --list-categories --filter "Deutsch" --debug
|
||||||
|
|
||||||
|
Hinweise:
|
||||||
|
- Include-Logik: + für UND, , für ODER. Keine Includes führt zu Fehler (da 'alle' nicht unterstützt).
|
||||||
|
- Duplikate in Kategorien werden automatisch entfernt und gecached, um redundante Abfragen zu vermeiden.
|
||||||
|
- Die API-Aufrufe enthalten Pausen (0.5s), um Rate-Limits zu respektieren.
|
||||||
|
- Große Kategorien oder Wortlisten (>10.000) können lange dauern.
|
||||||
|
- Stelle sicher, dass die Kategorien korrekt benannt sind (z.B. 'Substantiv (Deutsch)' für de.wiktionary.org).
|
||||||
|
EOT;
|
||||||
|
echo $help_text . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$is_cli = php_sapi_name() == 'cli';
|
||||||
|
|
||||||
|
if ($is_cli) {
|
||||||
|
$shortopts = "l:i:e:preferred-variant:f:o:d:c:r h";
|
||||||
|
$longopts = ["lang:", "include:", "exclude:", "preferred-variant:", "filter:", "format:", "debug", "list-categories", "headers", "help"];
|
||||||
|
$options = getopt($shortopts, $longopts);
|
||||||
|
|
||||||
|
// Positionale Argumente für CLI
|
||||||
|
$non_opt_args = array_filter($argv, function($arg) { return $arg[0] !== '-'; });
|
||||||
|
$non_opt_args = array_values(array_slice($non_opt_args, 1)); // Entferne Script-Namen
|
||||||
|
$options['num_words'] = $non_opt_args[0] ?? null;
|
||||||
|
$options['output_file'] = $non_opt_args[1] ?? null;
|
||||||
|
|
||||||
|
if (isset($options['help']) || isset($options['h'])) {
|
||||||
|
print_help();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lang = $options['lang'] ?? $options['l'] ?? 'en';
|
||||||
|
$include = $options['include'] ?? $options['i'] ?? '';
|
||||||
|
$exclude = $options['exclude'] ?? $options['e'] ?? '';
|
||||||
|
$preferred_variant = $options['preferred-variant'] ?? null;
|
||||||
|
$filter = $options['filter'] ?? $options['f'] ?? null;
|
||||||
|
$format = $options['format'] ?? $options['o'] ?? 'csv';
|
||||||
|
$debug = isset($options['debug']) || isset($options['d']);
|
||||||
|
$headers_flag = isset($options['headers']) || isset($options['r']);
|
||||||
|
$list_categories = isset($options['list-categories']) || isset($options['c']);
|
||||||
|
$num_words = (int)($options['num_words'] ?? 0);
|
||||||
|
$output_file = $options['output_file'] ?? 'output.csv';
|
||||||
|
|
||||||
|
$client = new WiktionaryClient($lang, $debug, $is_cli);
|
||||||
|
|
||||||
|
if ($list_categories) {
|
||||||
|
$client->list_all_categories($filter);
|
||||||
|
} else {
|
||||||
|
$client->generate_wordlist($include, $exclude, $num_words, $output_file, $preferred_variant, $headers_flag, $format);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Webserver-Kontext: Parameter aus $_GET
|
||||||
|
$options = $_GET;
|
||||||
|
$lang = $options['lang'] ?? 'en';
|
||||||
|
$include = $options['include'] ?? '';
|
||||||
|
$exclude = $options['exclude'] ?? '';
|
||||||
|
$preferred_variant = $options['preferred-variant'] ?? null;
|
||||||
|
$filter = $options['filter'] ?? null;
|
||||||
|
$format = $options['format'] ?? 'csv';
|
||||||
|
$debug = isset($options['debug']);
|
||||||
|
$headers_flag = isset($options['headers']);
|
||||||
|
$list_categories = isset($options['list-categories']);
|
||||||
|
$num_words = (int)($options['num_words'] ?? 0);
|
||||||
|
$output_file = $options['output_file'] ?? 'output.csv';
|
||||||
|
|
||||||
|
if (isset($options['help'])) {
|
||||||
|
header('Content-Type: text/plain; charset=UTF-8');
|
||||||
|
print_help();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$client = new WiktionaryClient($lang, $debug, $is_cli);
|
||||||
|
|
||||||
|
if ($list_categories) {
|
||||||
|
header('Content-Type: text/plain; charset=UTF-8');
|
||||||
|
$client->list_all_categories($filter);
|
||||||
|
} else {
|
||||||
|
$client->generate_wordlist($include, $exclude, $num_words, $output_file, $preferred_variant, $headers_flag, $format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
|
def debug_print(msg, debug=False):
|
||||||
|
if debug:
|
||||||
|
print(f"DEBUG: {msg}")
|
||||||
|
|
||||||
|
def print_help():
|
||||||
|
help_text = """
|
||||||
|
Wiktionary Wortliste Generator
|
||||||
|
|
||||||
|
Dieses Skript generiert eine Wortliste mit Begriffen und deren ersten Bedeutungen aus einer Wiktionary-Instanz
|
||||||
|
oder listet verfügbare Kategorien auf.
|
||||||
|
|
||||||
|
Verfügbare Argumente:
|
||||||
|
--lang, -l <Sprachcode> Sprachcode für die Wiktionary-Instanz (z.B. 'de' für de.wiktionary.org).
|
||||||
|
Default: 'en'. Erforderlich.
|
||||||
|
--include, -i <Logik> Logik für einzubeziehende Kategorien (z.B. 'A+B,C' für (A UND B) ODER C).
|
||||||
|
Erforderlich für Wortlisten (keine Includes führt zu Fehler), ignoriert bei --list-categories.
|
||||||
|
--exclude, -e <Kategorien> Komma-separierte Liste auszuschließender Kategorien (OR-Logik).
|
||||||
|
Optional, ignoriert bei --list-categories.
|
||||||
|
--preferred-variant <Name> Bevorzugte Schreibweise (z.B. 'Schweizerische' – extrahiert aus Excerpt und als erstes ausgegeben).
|
||||||
|
Optional.
|
||||||
|
--filter, -f <Text> Textfilter für Kategorien (Substring-Match, case-insensitive).
|
||||||
|
Optional, nur bei --list-categories relevant.
|
||||||
|
--debug, -d Aktiviert ausführliche Debug-Ausgaben für API-Aufrufe und Prozesse.
|
||||||
|
Optional.
|
||||||
|
--headers Aktiviert die Ausgabe der Spaltenüberschrift in der CSV-Datei (default: deaktiviert).
|
||||||
|
Optional, nur für Wortlisten relevant.
|
||||||
|
--list-categories, -c Listet alle Kategorien der Wiktionary-Instanz (optional mit --filter).
|
||||||
|
Wenn gesetzt, werden andere Parameter (außer --lang, --filter, --debug) ignoriert.
|
||||||
|
num_words Anzahl der Wörter für die Wortliste (Integer).
|
||||||
|
Erforderlich für Wortlisten, ignoriert bei --list-categories.
|
||||||
|
output_file Ausgabedatei für die Wortliste (CSV-Format).
|
||||||
|
Erforderlich für Wortlisten, ignoriert bei --list-categories. Default: 'output.csv'.
|
||||||
|
|
||||||
|
Argumentkombinationen:
|
||||||
|
1. Wortliste generieren:
|
||||||
|
- Erforderlich: --lang, --include, num_words, output_file
|
||||||
|
- Optional: --exclude, --preferred-variant, --debug, --headers
|
||||||
|
- Beispiel: python script.py --lang de --include "Substantiv (Deutsch)+Deutsch,Verb (Deutsch)" --exclude "Veraltete Substantive (Deutsch)" --preferred-variant "Schweizerische" 10000 output.csv --debug --headers
|
||||||
|
2. Kategorien auflisten:
|
||||||
|
- Erforderlich: --lang, --list-categories
|
||||||
|
- Optional: --filter, --debug
|
||||||
|
- Beispiel: python script.py --lang de --list-categories --filter "Deutsch" --debug
|
||||||
|
|
||||||
|
Hinweise:
|
||||||
|
- Include-Logik: + für UND, , für ODER. Keine Includes führt zu Fehler (da 'alle' nicht unterstützt).
|
||||||
|
- Duplikate in Kategorien werden automatisch entfernt und gecached, um redundante Abfragen zu vermeiden.
|
||||||
|
- Die API-Aufrufe enthalten Pausen (0.5s), um Rate-Limits zu respektieren.
|
||||||
|
- Große Kategorien oder Wortlisten (>10.000) können lange dauern.
|
||||||
|
- Stelle sicher, dass die Kategorien korrekt benannt sind (z.B. 'Substantiv (Deutsch)' für de.wiktionary.org).
|
||||||
|
"""
|
||||||
|
print(help_text)
|
||||||
|
|
||||||
|
def get_category_members(api_url, category, debug=False, category_cache={}):
|
||||||
|
if category in category_cache:
|
||||||
|
debug_print(f"Cache-Hit für Kategorie '{category}': {len(category_cache[category])} Member", debug)
|
||||||
|
return category_cache[category]
|
||||||
|
|
||||||
|
members = set()
|
||||||
|
params = {
|
||||||
|
'action': 'query',
|
||||||
|
'list': 'categorymembers',
|
||||||
|
'cmtitle': f'Category:{category}',
|
||||||
|
'cmtype': 'page',
|
||||||
|
'cmlimit': '500',
|
||||||
|
'format': 'json'
|
||||||
|
}
|
||||||
|
headers = {'User-Agent': 'GrokScript/1.0'}
|
||||||
|
debug_print(f"Starte Abruf für Kategorie '{category}' mit URL: {api_url} und Params: {params}", debug)
|
||||||
|
page = 1
|
||||||
|
total_members = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
response = requests.get(api_url, params=params, headers=headers)
|
||||||
|
debug_print(f"Response Status: {response.status_code}, Content-Length: {len(response.text)}", debug)
|
||||||
|
if response.status_code != 200:
|
||||||
|
print(f"Fehler beim Abrufen von {category}: {response.status_code}")
|
||||||
|
debug_print(f"Response Text: {response.text[:500]}...", debug)
|
||||||
|
break
|
||||||
|
data = response.json()
|
||||||
|
debug_print(f"Data Keys: {list(data.keys())}", debug)
|
||||||
|
new_members = [member['title'] for member in data.get('query', {}).get('categorymembers', [])]
|
||||||
|
members.update(new_members)
|
||||||
|
total_members += len(new_members)
|
||||||
|
print(f"\rKategorie '{category}' Paging: Seite {page} abgerufen ({len(new_members)} neue, total {total_members})", end='')
|
||||||
|
if 'continue' in data:
|
||||||
|
params['cmcontinue'] = data['continue']['cmcontinue']
|
||||||
|
debug_print(f"Paginierung fortsetzen mit cmcontinue: {params['cmcontinue']}", debug)
|
||||||
|
page += 1
|
||||||
|
else:
|
||||||
|
print() # Neue Zeile nach Abschluss
|
||||||
|
break
|
||||||
|
time.sleep(0.5) # Pause zur Rate-Limiting-Vermeidung
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler beim API-Aufruf für {category}: {str(e)}")
|
||||||
|
debug_print(f"Exception Details: {type(e).__name__} - {str(e)}", debug)
|
||||||
|
break
|
||||||
|
category_cache[category] = members
|
||||||
|
debug_print(f"Abgerufene Member für '{category}' (cached): {len(members)}", debug)
|
||||||
|
return members
|
||||||
|
|
||||||
|
def parse_include_logic(include_str):
|
||||||
|
if not include_str:
|
||||||
|
return []
|
||||||
|
groups = include_str.split(',')
|
||||||
|
parsed_groups = []
|
||||||
|
for group in groups:
|
||||||
|
cats = set(group.split('+')) # Set für unique in Gruppe
|
||||||
|
parsed_groups.append(list(cats))
|
||||||
|
return parsed_groups
|
||||||
|
|
||||||
|
def get_words_from_logic(api_url, include_groups, debug=False):
|
||||||
|
all_words = set()
|
||||||
|
for group in include_groups:
|
||||||
|
if not group:
|
||||||
|
continue
|
||||||
|
group_members = [get_category_members(api_url, cat, debug) for cat in group]
|
||||||
|
if group_members:
|
||||||
|
intersection = reduce(set.intersection, group_members)
|
||||||
|
all_words.update(intersection)
|
||||||
|
return all_words
|
||||||
|
|
||||||
|
def extract_definition_and_variant(extract, preferred_variant=None):
|
||||||
|
lines = extract.split('\n')
|
||||||
|
definition = 'Keine Definition gefunden'
|
||||||
|
variant = ''
|
||||||
|
in_bedeutungen = False
|
||||||
|
for line in lines:
|
||||||
|
if 'Bedeutungen:' in line:
|
||||||
|
in_bedeutungen = True
|
||||||
|
continue
|
||||||
|
if in_bedeutungen:
|
||||||
|
if line.startswith('[1] '):
|
||||||
|
definition = line[4:].strip()
|
||||||
|
break
|
||||||
|
if line.startswith('[1]'):
|
||||||
|
definition = line.split('[1]', 1)[1].strip()
|
||||||
|
break
|
||||||
|
# Alternative Schreibweise suchen
|
||||||
|
for line in lines:
|
||||||
|
if 'Alternative Schreibweisen:' in line or 'Nebenformen:' in line:
|
||||||
|
# Extrahiere Wörter nach Labels
|
||||||
|
parts = re.split(r'\w+ische Schreibweise:', line) # Split nach Labels wie "Schweizerische Schreibweise:"
|
||||||
|
for part in parts[1:]: # Ignoriere vor dem ersten Label
|
||||||
|
words = re.findall(r'[\w\-]+', part) # Extrahiere Wörter
|
||||||
|
if words:
|
||||||
|
if preferred_variant and preferred_variant.lower() in line.lower():
|
||||||
|
variant = words[0]
|
||||||
|
break
|
||||||
|
elif not preferred_variant:
|
||||||
|
variant = words[0]
|
||||||
|
break
|
||||||
|
if variant:
|
||||||
|
break
|
||||||
|
return definition, variant
|
||||||
|
|
||||||
|
def get_definitions_batch(api_url, words, debug=False):
|
||||||
|
definitions = {}
|
||||||
|
variants = {}
|
||||||
|
headers = {'User-Agent': 'GrokScript/1.0'}
|
||||||
|
for idx, word in enumerate(words):
|
||||||
|
params = {
|
||||||
|
'action': 'query',
|
||||||
|
'prop': 'extracts',
|
||||||
|
'titles': word,
|
||||||
|
'explaintext': '1',
|
||||||
|
'exsectionformat': 'plain',
|
||||||
|
'format': 'json'
|
||||||
|
}
|
||||||
|
debug_print(f"Starte Definitionsabruf für Wort {idx+1}/{len(words)}: '{word}' mit URL: {api_url} und Params: {params}", debug)
|
||||||
|
try:
|
||||||
|
response = requests.get(api_url, params=params, headers=headers)
|
||||||
|
debug_print(f"Response Status: {response.status_code}, Content-Length: {len(response.text)}", debug)
|
||||||
|
if response.status_code != 200:
|
||||||
|
print(f"Fehler beim Abrufen von Definition für '{word}': {response.status_code}")
|
||||||
|
debug_print(f"Response Text: {response.text[:500]}...", debug)
|
||||||
|
definitions[word] = 'Keine Definition gefunden'
|
||||||
|
variants[word] = ''
|
||||||
|
continue
|
||||||
|
data = response.json()
|
||||||
|
debug_print(f"Data Keys: {list(data.keys())}", debug)
|
||||||
|
page = next(iter(data.get('query', {}).get('pages', {}).values()), {})
|
||||||
|
extract = page.get('extract', '')
|
||||||
|
defn, var = extract_definition_and_variant(extract, args.preferred_variant)
|
||||||
|
definitions[word] = defn
|
||||||
|
variants[word] = var
|
||||||
|
print(f"\rDefinitionen abrufen: {idx+1}/{len(words)} abgeschlossen ({word})", end='')
|
||||||
|
time.sleep(0.5) # Pause pro Request zur Rate-Limiting-Vermeidung
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler beim Definitionsabruf für '{word}': {str(e)}")
|
||||||
|
debug_print(f"Exception Details: {type(e).__name__} - {str(e)}", debug)
|
||||||
|
definitions[word] = 'Keine Definition gefunden'
|
||||||
|
variants[word] = ''
|
||||||
|
print() # Neue Zeile nach Abschluss
|
||||||
|
debug_print(f"Abgerufene Definitionen: {len(definitions)}", debug)
|
||||||
|
return definitions, variants
|
||||||
|
|
||||||
|
def list_all_categories(api_url, filter_str=None, debug=False):
|
||||||
|
categories = []
|
||||||
|
params = {
|
||||||
|
'action': 'query',
|
||||||
|
'list': 'allcategories',
|
||||||
|
'aclimit': '500',
|
||||||
|
'format': 'json'
|
||||||
|
}
|
||||||
|
headers = {'User-Agent': 'GrokScript/1.0'}
|
||||||
|
debug_print(f"Starte Abruf aller Kategorien mit URL: {api_url} und Params: {params}", debug)
|
||||||
|
page = 1
|
||||||
|
total_categories = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
response = requests.get(api_url, params=params, headers=headers)
|
||||||
|
debug_print(f"Response Status: {response.status_code}, Content-Length: {len(response.text)}", debug)
|
||||||
|
if response.status_code != 200:
|
||||||
|
print("Fehler beim Abrufen der Kategorien")
|
||||||
|
debug_print(f"Response Text: {response.text[:500]}...", debug)
|
||||||
|
break
|
||||||
|
data = response.json()
|
||||||
|
debug_print(f"Data Keys: {list(data.keys())}", debug)
|
||||||
|
new_categories = [cat['*'] for cat in data.get('query', {}).get('allcategories', [])]
|
||||||
|
categories.extend(new_categories)
|
||||||
|
total_categories += len(new_categories)
|
||||||
|
print(f"\rKategorien Paging: Seite {page} abgerufen ({len(new_categories)} neue, total {total_categories})", end='')
|
||||||
|
if 'continue' in data:
|
||||||
|
params['accontinue'] = data['continue']['accontinue']
|
||||||
|
debug_print(f"Paginierung fortsetzen mit accontinue: {params['accontinue']}", debug)
|
||||||
|
page += 1
|
||||||
|
else:
|
||||||
|
print() # Neue Zeile nach Abschluss
|
||||||
|
break
|
||||||
|
time.sleep(0.5) # Pause
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler beim API-Aufruf für Kategorien: {str(e)}")
|
||||||
|
debug_print(f"Exception Details: {type(e).__name__} - {str(e)}", debug)
|
||||||
|
break
|
||||||
|
if filter_str:
|
||||||
|
filter_lower = filter_str.lower()
|
||||||
|
categories = [cat for cat in categories if filter_lower in cat.lower()]
|
||||||
|
debug_print(f"Gefiltert nach '{filter_str}': {len(categories)} Kategorien gefunden", debug)
|
||||||
|
|
||||||
|
print("Vorhandene Kategorien" + (f" (gefiltert nach '{filter_str}')" if filter_str else "") + ":")
|
||||||
|
for cat in sorted(categories):
|
||||||
|
print(cat)
|
||||||
|
debug_print(f"Gesamte Kategorien: {len(categories)}", debug)
|
||||||
|
return categories
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
category_cache = {} # Globaler Cache für Kategorien
|
||||||
|
if args.help:
|
||||||
|
print_help()
|
||||||
|
return
|
||||||
|
|
||||||
|
api_url = f'https://{args.lang}.wiktionary.org/w/api.php'
|
||||||
|
debug_print(f"API-URL: {api_url}", args.debug)
|
||||||
|
|
||||||
|
if args.list_categories:
|
||||||
|
list_all_categories(api_url, args.filter, args.debug)
|
||||||
|
return
|
||||||
|
|
||||||
|
include_groups = parse_include_logic(args.include)
|
||||||
|
if not include_groups:
|
||||||
|
print("Fehler: --include muss angegeben werden, da 'alle' nicht unterstützt wird.")
|
||||||
|
return
|
||||||
|
|
||||||
|
all_words = get_words_from_logic(api_url, include_groups, args.debug)
|
||||||
|
debug_print(f"Include-Wörter nach Logik: {len(all_words)}", args.debug)
|
||||||
|
|
||||||
|
exclude_cats = list(set([cat.strip() for cat in args.exclude.split(',') if cat.strip()])) if args.exclude else [] # Unique exclude
|
||||||
|
exclude_words = set()
|
||||||
|
for cat in exclude_cats:
|
||||||
|
print(f"Abrufen von Kategorie (exclude): {cat}")
|
||||||
|
members = get_category_members(api_url, cat, args.debug, category_cache)
|
||||||
|
exclude_words.update(members)
|
||||||
|
debug_print(f"Exclude-Wörter: {len(exclude_words)}", args.debug)
|
||||||
|
|
||||||
|
all_words -= exclude_words
|
||||||
|
debug_print(f"Finale Wörter nach Exclude: {len(all_words)}", args.debug)
|
||||||
|
|
||||||
|
if not all_words:
|
||||||
|
print("Keine Wörter gefunden. Überprüfe die Kategorien.")
|
||||||
|
return
|
||||||
|
|
||||||
|
words_list = list(all_words)
|
||||||
|
random.shuffle(words_list)
|
||||||
|
selected_words = words_list[:args.num_words]
|
||||||
|
debug_print(f"Ausgewählte Wörter: {len(selected_words)}", args.debug)
|
||||||
|
|
||||||
|
print(f"Abrufen von Definitionen für {len(selected_words)} Wörter...")
|
||||||
|
definitions, variants = get_definitions_batch(api_url, selected_words, args.debug)
|
||||||
|
|
||||||
|
# Erstelle Tabelle (als CSV-Datei)
|
||||||
|
with open(args.output_file, 'w', encoding='utf-8') as f:
|
||||||
|
if args.headers:
|
||||||
|
f.write("Begriff\tErste Bedeutung\n")
|
||||||
|
for word in selected_words:
|
||||||
|
var = variants.get(word, '')
|
||||||
|
begriff = f"{var}, {word}" if var else word
|
||||||
|
defn = definitions.get(word, 'Keine Definition gefunden')
|
||||||
|
f.write(f"{begriff}\t{defn}\n")
|
||||||
|
print(f"Tabelle gespeichert in {args.output_file}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="Wiktionary Wortliste Generator", add_help=False)
|
||||||
|
parser.add_argument('--lang', '-l', default='en', help='Sprachcode für Wiktionary (z.B. de für de.wiktionary.org)')
|
||||||
|
parser.add_argument('--include', '-i', default='', help='Logik für einzubeziehende Kategorien (z.B. A+B,C)')
|
||||||
|
parser.add_argument('--exclude', '-e', default='', help='Komma-separierte Liste auszuschließender Kategorien')
|
||||||
|
parser.add_argument('--preferred-variant', default=None, help='Bevorzugte Schreibweise (z.B. Schweizerische)')
|
||||||
|
parser.add_argument('num_words', type=int, nargs='?', default=0, help='Anzahl der Wörter')
|
||||||
|
parser.add_argument('output_file', nargs='?', default='output.csv', help='Ausgabedatei (CSV)')
|
||||||
|
parser.add_argument('--list-categories', '-c', action='store_true', help='Liste alle Kategorien der Wiktionary-Instanz')
|
||||||
|
parser.add_argument('--filter', '-f', default=None, help='Textfilter für Kategorien (Substring-Match, case-insensitive)')
|
||||||
|
parser.add_argument('--debug', '-d', action='store_true', help='Aktiviere Debug-Ausgaben')
|
||||||
|
parser.add_argument('--headers', action='store_true', help='Aktiviere Spaltenüberschrift in der Ausgabe')
|
||||||
|
parser.add_argument('--help', '-h', action='store_true', help='Zeige diese Hilfe an')
|
||||||
|
args = parser.parse_args()
|
||||||
|
main(args)
|
||||||
@@ -0,0 +1,410 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Wiktionary Random Word Generator 4.2 (Korrigierte Version)
|
||||||
|
|
||||||
|
Erstellt zufällige Wortlisten mit Definitionen aus Wiktionary.
|
||||||
|
- Echte Zufälligkeit durch MediaWiki Random API
|
||||||
|
- Integrierte Definition-Extraktion
|
||||||
|
- Kategorie-Filter und Zahlen-Ausschluss
|
||||||
|
- Debug-Modus für Fehlerdiagnose
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import time
|
||||||
|
import csv
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Optional, Dict
|
||||||
|
|
||||||
|
class WiktionaryRandomGenerator:
|
||||||
|
def __init__(self, language: str, debug: bool = False):
|
||||||
|
self.language = language
|
||||||
|
self.debug = debug
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.headers.update({'User-Agent': 'Wiktionary Generator 4.2'})
|
||||||
|
|
||||||
|
# API URLs
|
||||||
|
self.api_url = {
|
||||||
|
'de': 'https://de.wiktionary.org/w/api.php',
|
||||||
|
'en': 'https://en.wiktionary.org/w/api.php'
|
||||||
|
}[language]
|
||||||
|
|
||||||
|
# Wortarten-Kategorien (für Filterung)
|
||||||
|
self.pos_categories = {
|
||||||
|
'de': {
|
||||||
|
'substantiv': 'Kategorie:Substantiv (Deutsch)',
|
||||||
|
'verb': 'Kategorie:Verb (Deutsch)',
|
||||||
|
'adjektiv': 'Kategorie:Adjektiv (Deutsch)',
|
||||||
|
'adverb': 'Kategorie:Adverb (Deutsch)',
|
||||||
|
'artikel': 'Kategorie:Artikel (Deutsch)',
|
||||||
|
'pronomen': 'Kategorie:Pronomen (Deutsch)',
|
||||||
|
'präposition': 'Kategorie:Präposition (Deutsch)',
|
||||||
|
'konjunktion': 'Kategorie:Konjunktion (Deutsch)',
|
||||||
|
'interjektion': 'Kategorie:Interjektion (Deutsch)'
|
||||||
|
},
|
||||||
|
'en': {
|
||||||
|
'noun': 'Category:English nouns',
|
||||||
|
'verb': 'Category:English verbs',
|
||||||
|
'adjective': 'Category:English adjectives',
|
||||||
|
'adverb': 'Category:English adverbs',
|
||||||
|
'pronoun': 'Category:English pronouns',
|
||||||
|
'preposition': 'Category:English prepositions',
|
||||||
|
'conjunction': 'Category:English conjunctions',
|
||||||
|
'interjection': 'Category:English interjections'
|
||||||
|
}
|
||||||
|
}[language]
|
||||||
|
|
||||||
|
# Ausschluss-Kategorien (Namen, Geografie)
|
||||||
|
self.excluded_categories = {
|
||||||
|
'de': {
|
||||||
|
'Kategorie:Eigenname (Deutsch)',
|
||||||
|
'Kategorie:Vorname (Deutsch)',
|
||||||
|
'Kategorie:Nachname (Deutsch)',
|
||||||
|
'Kategorie:Toponym (Deutsch)',
|
||||||
|
'Kategorie:Stadt (Deutsch)',
|
||||||
|
'Kategorie:Land (Deutsch)'
|
||||||
|
},
|
||||||
|
'en': {
|
||||||
|
'Category:English proper nouns',
|
||||||
|
'Category:English given names',
|
||||||
|
'Category:English surnames'
|
||||||
|
}
|
||||||
|
}[language]
|
||||||
|
|
||||||
|
# Standard-Wortarten
|
||||||
|
self.default_pos = ['substantiv', 'verb', 'adjektiv'] if language == 'de' else ['noun', 'verb', 'adjective']
|
||||||
|
|
||||||
|
self.debug_print(f"Initialisiert für Sprache: {language}")
|
||||||
|
self.debug_print(f"API URL: {self.api_url}")
|
||||||
|
self.debug_print(f"Standard-Wortarten: {self.default_pos}")
|
||||||
|
|
||||||
|
def debug_print(self, msg: str):
|
||||||
|
if self.debug:
|
||||||
|
print(f"[DEBUG] {msg}")
|
||||||
|
|
||||||
|
def has_numbers(self, word: str) -> bool:
|
||||||
|
"""Prüft ob ein Wort Ziffern enthält"""
|
||||||
|
result = bool(re.search(r'\d', word))
|
||||||
|
if result and self.debug:
|
||||||
|
self.debug_print(f"Wort '{word}' enthält Zahlen -> ausgeschlossen")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def clean_definition(self, text: str) -> str:
|
||||||
|
"""Bereinigt Definition von Wiki-Markup und Formatierung"""
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
original = text
|
||||||
|
# Entferne führende Nummerierungen: [1], 2), etc.
|
||||||
|
text = re.sub(r'^\s*\[?\d+\]?\s*[\]\)]*\s*', '', text)
|
||||||
|
# Entferne führende Sonderzeichen: :, •, -, etc.
|
||||||
|
text = re.sub(r'^\s*[:\-\*\+>•]+\s*', '', text)
|
||||||
|
# Entferne führende Klammern/Anführungszeichen
|
||||||
|
text = re.sub(r'^\s*[\(\[\"\'`]+\s*', '', text)
|
||||||
|
# Normalisiere Leerzeichen
|
||||||
|
text = re.sub(r'\s+', ' ', text).strip()
|
||||||
|
|
||||||
|
# Kleinschreibung am Anfang (außer bei Eigennamen)
|
||||||
|
if text and len(text) > 1 and text[0].isupper() and text[1].islower():
|
||||||
|
text = text[0].lower() + text[1:]
|
||||||
|
|
||||||
|
if self.debug and original != text:
|
||||||
|
self.debug_print(f"Definition bereinigt: '{original[:30]}...' -> '{text[:30]}...'")
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
def fetch_random_pages(self, count: int) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Holt zufällige Seiten mit Definitionen und Kategorien.
|
||||||
|
Verwendet zwei API-Calls: random + details
|
||||||
|
"""
|
||||||
|
self.debug_print(f"Hole {count} zufällige Seiten")
|
||||||
|
|
||||||
|
# Schritt 1: Hole zufällige Page-IDs
|
||||||
|
try:
|
||||||
|
params = {
|
||||||
|
'action': 'query',
|
||||||
|
'format': 'json',
|
||||||
|
'list': 'random',
|
||||||
|
'rnlimit': min(count, 500),
|
||||||
|
'rnnamespace': 0 # Nur Hauptnamensraum
|
||||||
|
}
|
||||||
|
|
||||||
|
self.debug_print(f"Random API Call: {params}")
|
||||||
|
response = self.session.get(self.api_url, params=params, timeout=15)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
if 'query' not in data or 'random' not in data['query']:
|
||||||
|
self.debug_print(f"Fehler in Random API Response: {data}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
random_pages = data['query']['random']
|
||||||
|
page_ids = [str(page['id']) for page in random_pages]
|
||||||
|
titles = [page['title'] for page in random_pages]
|
||||||
|
|
||||||
|
self.debug_print(f"Gefunden: {len(page_ids)} zufällige Seiten")
|
||||||
|
|
||||||
|
if not page_ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.debug_print(f"Fehler beim Random API Call: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Schritt 2: Hole Details (Extract + Categories) für diese IDs
|
||||||
|
try:
|
||||||
|
params = {
|
||||||
|
'action': 'query',
|
||||||
|
'format': 'json',
|
||||||
|
'pageids': '|'.join(page_ids),
|
||||||
|
'prop': 'extracts|categories',
|
||||||
|
'exintro': True,
|
||||||
|
'explaintext': True,
|
||||||
|
'cllimit': 500
|
||||||
|
}
|
||||||
|
|
||||||
|
self.debug_print(f"Details API Call: {params}")
|
||||||
|
response = self.session.get(self.api_url, params=params, timeout=15)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
if 'query' not in data or 'pages' not in data['query']:
|
||||||
|
self.debug_print(f"Fehler in Details API Response: {data}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
pages = data['query']['pages']
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for page_id, page_data in pages.items():
|
||||||
|
if page_id == '-1': # Ungültige Seite
|
||||||
|
continue
|
||||||
|
|
||||||
|
title = page_data.get('title', '')
|
||||||
|
extract = page_data.get('extract', '')
|
||||||
|
categories = set()
|
||||||
|
|
||||||
|
# Kategorien sammeln
|
||||||
|
if 'categories' in page_data:
|
||||||
|
categories = {cat['title'] for cat in page_data['categories']}
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'title': title,
|
||||||
|
'extract': extract,
|
||||||
|
'categories': categories
|
||||||
|
})
|
||||||
|
|
||||||
|
self.debug_print(f"Seite '{title}': Extract={len(extract)} chars, Categories={len(categories)}")
|
||||||
|
|
||||||
|
self.debug_print(f"Details für {len(results)} Seiten geholt")
|
||||||
|
return results
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.debug_print(f"Fehler beim Details API Call: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def check_word_categories(self, categories: set, include_pos: List[str]) -> bool:
|
||||||
|
"""
|
||||||
|
Prüft ob ein Wort in den gewünschten Wortart-Kategorien ist.
|
||||||
|
Wenn include_pos leer ist, werden alle Wörter akzeptiert.
|
||||||
|
"""
|
||||||
|
if not include_pos:
|
||||||
|
return True # Keine POS-Filter
|
||||||
|
|
||||||
|
# Prüfe ob eine der gewünschten Kategorien vorhanden ist
|
||||||
|
desired_categories = {self.pos_categories[pos] for pos in include_pos
|
||||||
|
if pos in self.pos_categories}
|
||||||
|
|
||||||
|
has_desired_category = bool(categories & desired_categories)
|
||||||
|
|
||||||
|
if self.debug:
|
||||||
|
if has_desired_category:
|
||||||
|
matching = categories & desired_categories
|
||||||
|
self.debug_print(f"Wort in gewünschter Kategorie: {matching}")
|
||||||
|
else:
|
||||||
|
self.debug_print(f"Wort nicht in gewünschten Kategorien: {desired_categories}")
|
||||||
|
|
||||||
|
return has_desired_category
|
||||||
|
|
||||||
|
def is_excluded_word(self, categories: set) -> bool:
|
||||||
|
"""Prüft ob ein Wort in Ausschluss-Kategorien ist"""
|
||||||
|
excluded = bool(categories & self.excluded_categories)
|
||||||
|
if excluded and self.debug:
|
||||||
|
matching = categories & self.excluded_categories
|
||||||
|
self.debug_print(f"Wort ausgeschlossen wegen: {matching}")
|
||||||
|
return excluded
|
||||||
|
|
||||||
|
def generate(self, count: int, include_pos: List[str], include_names: bool,
|
||||||
|
exclude_numbers: bool, outfile: str):
|
||||||
|
"""Generiert die Wortliste"""
|
||||||
|
|
||||||
|
pos = include_pos or []
|
||||||
|
self.debug_print(f"Parameter: count={count}, pos={pos}, names={include_names}, numbers={exclude_numbers}")
|
||||||
|
|
||||||
|
print(f"Generiere {count} zufällige Wörter aus {self.language.upper()}-Wiktionary")
|
||||||
|
if pos:
|
||||||
|
print(f"Wortarten-Filter: {', '.join(pos)}")
|
||||||
|
else:
|
||||||
|
print("Keine Wortarten-Filter (alle Wörter)")
|
||||||
|
print(f"Namen einschließen: {'Ja' if include_names else 'Nein'}")
|
||||||
|
print(f"Zahlen ausschließen: {'Ja' if exclude_numbers else 'Nein'}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
attempts = 0
|
||||||
|
max_attempts = count * 5 # Maximal 5x so viele Versuche
|
||||||
|
|
||||||
|
print("Sammle zufällige Kandidaten...")
|
||||||
|
|
||||||
|
while len(results) < count and attempts < max_attempts:
|
||||||
|
batch_size = min(50, (count - len(results)) * 2) # Hole mehr als nötig
|
||||||
|
candidates = self.fetch_random_pages(batch_size)
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
self.debug_print("Keine Kandidaten erhalten")
|
||||||
|
break
|
||||||
|
|
||||||
|
self.debug_print(f"Verarbeite {len(candidates)} Kandidaten...")
|
||||||
|
|
||||||
|
for candidate in candidates:
|
||||||
|
if len(results) >= count:
|
||||||
|
break
|
||||||
|
|
||||||
|
title = candidate['title']
|
||||||
|
extract = candidate['extract']
|
||||||
|
categories = candidate['categories']
|
||||||
|
|
||||||
|
self.debug_print(f"Prüfe Kandidat: '{title}'")
|
||||||
|
|
||||||
|
# Filter 1: Namen ausschließen (falls gewünscht)
|
||||||
|
if not include_names and self.is_excluded_word(categories):
|
||||||
|
self.debug_print(f"'{title}' wegen Namen-Filter übersprungen")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Filter 2: Zahlen ausschließen (falls gewünscht)
|
||||||
|
if exclude_numbers and self.has_numbers(title):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Filter 3: Wortarten-Filter (falls angegeben)
|
||||||
|
if pos and not self.check_word_categories(categories, pos):
|
||||||
|
self.debug_print(f"'{title}' wegen POS-Filter übersprungen")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Filter 4: Definition muss vorhanden sein
|
||||||
|
if not extract or len(extract) < 10:
|
||||||
|
self.debug_print(f"'{title}' hat keine ausreichende Definition")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Definition bereinigen
|
||||||
|
cleaned_definition = self.clean_definition(extract)
|
||||||
|
if len(cleaned_definition) < 10:
|
||||||
|
self.debug_print(f"'{title}' Definition zu kurz nach Bereinigung")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Wort akzeptieren
|
||||||
|
results.append((title, cleaned_definition))
|
||||||
|
print(f" {len(results):2d}/{count}: {title}")
|
||||||
|
|
||||||
|
self.debug_print(f"'{title}' erfolgreich hinzugefügt")
|
||||||
|
|
||||||
|
attempts += batch_size
|
||||||
|
|
||||||
|
# Kleine Pause zwischen Batches
|
||||||
|
if len(results) < count:
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
print("FEHLER: Keine Wörter generiert! Versuchen Sie:")
|
||||||
|
print("- --include-names für mehr Kandidaten")
|
||||||
|
print("- Andere/keine --include-pos Filter")
|
||||||
|
print("- --debug für detaillierte Ausgabe")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Finale Mischung und Begrenzung
|
||||||
|
random.shuffle(results)
|
||||||
|
results = results[:count]
|
||||||
|
|
||||||
|
print(f"\n✅ Erfolgreich {len(results)} Wörter generiert!")
|
||||||
|
print(f"💾 Speichere in '{outfile}'...")
|
||||||
|
|
||||||
|
# CSV-Datei schreiben
|
||||||
|
try:
|
||||||
|
with open(outfile, 'w', encoding='utf-8', newline='') as f:
|
||||||
|
writer = csv.writer(f, delimiter='\t')
|
||||||
|
writer.writerow(['Wort', 'Definition', 'Sprache', 'Zeitstempel'])
|
||||||
|
|
||||||
|
for word, definition in results:
|
||||||
|
writer.writerow([
|
||||||
|
word,
|
||||||
|
definition,
|
||||||
|
self.language,
|
||||||
|
datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
])
|
||||||
|
|
||||||
|
print(f"✅ Datei '{outfile}' erfolgreich erstellt!")
|
||||||
|
|
||||||
|
# Beispiele anzeigen
|
||||||
|
print("\n📖 Beispiele aus der generierten Liste:")
|
||||||
|
print("-" * 65)
|
||||||
|
for i, (word, definition) in enumerate(results[:5], 1):
|
||||||
|
short_def = definition[:50] + "..." if len(definition) > 50 else definition
|
||||||
|
print(f"{i:2d}. {word:15s} → {short_def}")
|
||||||
|
|
||||||
|
if len(results) > 5:
|
||||||
|
print(f" ... und {len(results) - 5} weitere Wörter")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ FEHLER beim Schreiben der Datei: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Wiktionary Random Word Generator 4.2',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
Beispiele:
|
||||||
|
python script.py -c 30 -l de
|
||||||
|
python script.py -c 50 --include-pos substantiv verb
|
||||||
|
python script.py -c 25 -l en --exclude-numbers
|
||||||
|
python script.py -c 40 --include-names --debug
|
||||||
|
|
||||||
|
Verfügbare Wortarten:
|
||||||
|
Deutsch: substantiv, verb, adjektiv, adverb, artikel, pronomen, präposition, konjunktion, interjektion
|
||||||
|
Englisch: noun, verb, adjective, adverb, pronoun, preposition, conjunction, interjection
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument('-c', '--count', type=int, default=50,
|
||||||
|
help='Anzahl der zu generierenden Wörter (Standard: 50)')
|
||||||
|
parser.add_argument('-l', '--language', choices=['de', 'en'], default='de',
|
||||||
|
help='Wiktionary-Sprache (Standard: de)')
|
||||||
|
parser.add_argument('-o', '--output', default='wiktionary_words.csv',
|
||||||
|
help='Output-Datei (Standard: wiktionary_words.csv)')
|
||||||
|
parser.add_argument('--include-pos', nargs='+',
|
||||||
|
help='Nur diese Wortarten einschließen')
|
||||||
|
parser.add_argument('--include-names', action='store_true',
|
||||||
|
help='Namen und geografische Bezeichnungen einschließen')
|
||||||
|
parser.add_argument('--exclude-numbers', action='store_true',
|
||||||
|
help='Wörter mit Ziffern ausschließen')
|
||||||
|
parser.add_argument('--debug', action='store_true',
|
||||||
|
help='Debug-Modus für detaillierte Ausgabe')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Validierung
|
||||||
|
if args.count <= 0:
|
||||||
|
print("❌ Anzahl muss größer als 0 sein!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Generator erstellen und ausführen
|
||||||
|
gen = WiktionaryRandomGenerator(args.language, debug=args.debug)
|
||||||
|
gen.generate(
|
||||||
|
count=args.count,
|
||||||
|
include_pos=args.include_pos,
|
||||||
|
include_names=args.include_names,
|
||||||
|
exclude_numbers=args.exclude_numbers,
|
||||||
|
outfile=args.output
|
||||||
|
)
|
||||||
Reference in new issue
Block a user