Skills
This commit is contained in:
parent
3ffeec1344
commit
334dd87994
507
skills/django-templates/SKILL.md
Normal file
507
skills/django-templates/SKILL.md
Normal file
@ -0,0 +1,507 @@
|
||||
---
|
||||
name: django-templates
|
||||
description: Creates Django 6.0 HTML templates with partials, HTMX integration, and modern best practices. Use for template creation, refactoring, or HTMX endpoints.
|
||||
argument-hint: [template-name] [--partial|--htmx|--base]
|
||||
allowed-tools: Read, Write, Edit, Glob, Grep
|
||||
---
|
||||
|
||||
# Django 6.0 HTML Templates
|
||||
|
||||
Generate production-ready Django 6.0.2 templates with Template Partials, HTMX integration, CSP support, and modern best practices for the league-planner project.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Creating new HTML templates with Django 6.0 features
|
||||
- Refactoring templates to use Template Partials
|
||||
- Building HTMX-powered dynamic components
|
||||
- Setting up base templates with proper block structure
|
||||
- Implementing reusable template fragments
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Django 6.0+ installed
|
||||
- Templates directory configured in settings
|
||||
- For HTMX: `django-htmx` package (optional but recommended)
|
||||
|
||||
## Instructions
|
||||
|
||||
### Step 1: Analyze Request
|
||||
|
||||
Parse `$ARGUMENTS` to determine:
|
||||
- **Template name**: The target template file
|
||||
- **Type flag**:
|
||||
- `--partial`: Create reusable partial fragments
|
||||
- `--htmx`: HTMX-enabled template with partial endpoints
|
||||
- `--base`: Base template with block structure
|
||||
- (none): Standard template
|
||||
|
||||
### Step 2: Check Existing Templates
|
||||
|
||||
```bash
|
||||
# Find existing templates in league-planner
|
||||
find . -path "*/templates/*.html" -type f
|
||||
```
|
||||
|
||||
Review the project's template structure and naming conventions.
|
||||
|
||||
### Step 3: Generate Template
|
||||
|
||||
Apply the appropriate pattern from the examples below.
|
||||
|
||||
---
|
||||
|
||||
## Django 6.0 Template Features
|
||||
|
||||
### Template Partials (NEW in 6.0)
|
||||
|
||||
Define reusable fragments without separate files:
|
||||
|
||||
```django
|
||||
{# Define a partial #}
|
||||
{% partialdef card %}
|
||||
<div class="card">
|
||||
<h3>{{ title }}</h3>
|
||||
<p>{{ content }}</p>
|
||||
</div>
|
||||
{% endpartialdef %}
|
||||
|
||||
{# Render the partial multiple times #}
|
||||
{% partial card %}
|
||||
{% partial card %}
|
||||
```
|
||||
|
||||
**Inline Option** - Render immediately AND save for reuse:
|
||||
|
||||
```django
|
||||
{% partialdef filter_controls inline %}
|
||||
<form method="get" class="filters">
|
||||
{{ filter_form.as_p }}
|
||||
<button type="submit">Filter</button>
|
||||
</form>
|
||||
{% endpartialdef %}
|
||||
|
||||
{# Can still reuse later #}
|
||||
{% partial filter_controls %}
|
||||
```
|
||||
|
||||
### Accessing Partials from Views (HTMX Pattern)
|
||||
|
||||
Render only a specific partial:
|
||||
|
||||
```python
|
||||
# views.py
|
||||
from django.shortcuts import render
|
||||
|
||||
def update_component(request, pk):
|
||||
obj = MyModel.objects.get(pk=pk)
|
||||
# Render ONLY the partial named "item_row"
|
||||
return render(request, "myapp/list.html#item_row", {"item": obj})
|
||||
```
|
||||
|
||||
### forloop.length (NEW in 6.0)
|
||||
|
||||
Access total loop count:
|
||||
|
||||
```django
|
||||
{% for item in items %}
|
||||
<div>
|
||||
{{ item.name }}
|
||||
<span class="counter">({{ forloop.counter }}/{{ forloop.length }})</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
### querystring Tag Improvements
|
||||
|
||||
Build query strings cleanly:
|
||||
|
||||
```django
|
||||
{# Basic usage #}
|
||||
<a href="?{% querystring page=1 %}">First Page</a>
|
||||
|
||||
{# Modify current query params #}
|
||||
<a href="?{% querystring page=page.next_page_number %}">Next</a>
|
||||
|
||||
{# Remove a parameter #}
|
||||
<a href="?{% querystring filter=None %}">Clear Filter</a>
|
||||
|
||||
{# Multiple mappings (NEW in 6.0) #}
|
||||
<a href="?{% querystring base_params extra_params page=1 %}">Reset</a>
|
||||
```
|
||||
|
||||
### CSP Nonce Support
|
||||
|
||||
For inline scripts with Content Security Policy:
|
||||
|
||||
```django
|
||||
{# In settings: add 'django.template.context_processors.csp' #}
|
||||
|
||||
<script nonce="{{ csp_nonce }}">
|
||||
// Safe inline script
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Patterns & Best Practices
|
||||
|
||||
### Pattern 1: Base Template with Blocks
|
||||
|
||||
```django
|
||||
{# templates/base.html #}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}League Planner{% endblock %}</title>
|
||||
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block navbar %}
|
||||
{% include "includes/navbar.html" %}
|
||||
{% endblock %}
|
||||
|
||||
<main class="container">
|
||||
{% block messages %}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
{% block footer %}
|
||||
{% include "includes/footer.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Pattern 2: List Template with Partials
|
||||
|
||||
```django
|
||||
{# templates/scheduler/scenario_list.html #}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Szenarien{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="scenario-list">
|
||||
<h1>Szenarien</h1>
|
||||
|
||||
{# Define row partial for HTMX updates #}
|
||||
{% partialdef scenario_row %}
|
||||
<tr id="scenario-{{ scenario.pk }}">
|
||||
<td>{{ scenario.name }}</td>
|
||||
<td>{{ scenario.season }}</td>
|
||||
<td>{{ scenario.get_status_display }}</td>
|
||||
<td>
|
||||
<a href="{% url 'scenario-detail' scenario.pk %}"
|
||||
class="btn btn-sm btn-primary">Details</a>
|
||||
<button hx-delete="{% url 'scenario-delete' scenario.pk %}"
|
||||
hx-target="#scenario-{{ scenario.pk }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Wirklich löschen?"
|
||||
class="btn btn-sm btn-danger">
|
||||
Löschen
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endpartialdef %}
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Saison</th>
|
||||
<th>Status</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="scenario-table-body">
|
||||
{% for scenario in scenarios %}
|
||||
{% partial scenario_row %}
|
||||
{% empty %}
|
||||
<tr><td colspan="4">Keine Szenarien vorhanden.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### Pattern 3: HTMX-Powered Form
|
||||
|
||||
```django
|
||||
{# templates/scheduler/scenario_form.html #}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container">
|
||||
<h1>{% if scenario.pk %}Szenario bearbeiten{% else %}Neues Szenario{% endif %}</h1>
|
||||
|
||||
{# Form partial for HTMX validation #}
|
||||
{% partialdef scenario_form inline %}
|
||||
<form method="post"
|
||||
hx-post="{% url 'scenario-create' %}"
|
||||
hx-target="#form-container"
|
||||
hx-swap="innerHTML">
|
||||
{% csrf_token %}
|
||||
|
||||
{% for field in form %}
|
||||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% if field.errors %}
|
||||
<span class="error">{{ field.errors.0 }}</span>
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<small class="help-text">{{ field.help_text }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% if scenario.pk %}Speichern{% else %}Erstellen{% endif %}
|
||||
</button>
|
||||
<a href="{% url 'scenario-list' %}" class="btn btn-secondary">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endpartialdef %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### Pattern 4: Detail Page with Inline Editing
|
||||
|
||||
```django
|
||||
{# templates/scheduler/scenario_detail.html #}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<article class="scenario-detail">
|
||||
{# Header partial - editable via HTMX #}
|
||||
{% partialdef scenario_header inline %}
|
||||
<header id="scenario-header">
|
||||
<h1>{{ scenario.name }}</h1>
|
||||
<p class="meta">
|
||||
Saison: {{ scenario.season }} |
|
||||
Status: {{ scenario.get_status_display }} |
|
||||
Erstellt: {{ scenario.created_at|date:"d.m.Y H:i" }}
|
||||
</p>
|
||||
<button hx-get="{% url 'scenario-edit-header' scenario.pk %}"
|
||||
hx-target="#scenario-header"
|
||||
hx-swap="outerHTML"
|
||||
class="btn btn-sm btn-outline">
|
||||
Bearbeiten
|
||||
</button>
|
||||
</header>
|
||||
{% endpartialdef %}
|
||||
|
||||
{# Games list partial #}
|
||||
{% partialdef games_list %}
|
||||
<section id="games-list" class="games">
|
||||
<h2>Spiele ({{ games|length }}/{{ games|length }})</h2>
|
||||
<ul>
|
||||
{% for game in games %}
|
||||
<li>
|
||||
{{ game.home_team }} vs {{ game.away_team }}
|
||||
<span class="date">{{ game.date|date:"d.m.Y" }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% endpartialdef %}
|
||||
|
||||
{% partial games_list %}
|
||||
</article>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### Pattern 5: Modal with Partial
|
||||
|
||||
```django
|
||||
{# templates/includes/modal.html #}
|
||||
{% partialdef modal_container %}
|
||||
<div id="modal-container"
|
||||
class="modal {% if show %}show{% endif %}"
|
||||
hx-swap-oob="true">
|
||||
{% if show %}
|
||||
<div class="modal-backdrop" hx-get="" hx-target="#modal-container"></div>
|
||||
<div class="modal-content">
|
||||
{% block modal_header %}
|
||||
<header class="modal-header">
|
||||
<h3>{{ modal_title }}</h3>
|
||||
<button hx-get="" hx-target="#modal-container" class="close">×</button>
|
||||
</header>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal_body %}
|
||||
{{ modal_content }}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal_footer %}
|
||||
<footer class="modal-footer">
|
||||
<button hx-get="" hx-target="#modal-container" class="btn btn-secondary">
|
||||
Abbrechen
|
||||
</button>
|
||||
{% block modal_actions %}{% endblock %}
|
||||
</footer>
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endpartialdef %}
|
||||
```
|
||||
|
||||
### Pattern 6: Pagination with querystring
|
||||
|
||||
```django
|
||||
{# templates/includes/pagination.html #}
|
||||
{% if page_obj.has_other_pages %}
|
||||
<nav class="pagination" aria-label="Seitennavigation">
|
||||
<ul>
|
||||
{% if page_obj.has_previous %}
|
||||
<li>
|
||||
<a href="?{% querystring page=1 %}">« Erste</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="?{% querystring page=page_obj.previous_page_number %}">‹ Zurück</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="current">
|
||||
Seite {{ page_obj.number }} von {{ page_obj.paginator.num_pages }}
|
||||
</li>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li>
|
||||
<a href="?{% querystring page=page_obj.next_page_number %}">Weiter ›</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="?{% querystring page=page_obj.paginator.num_pages %}">Letzte »</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HTMX View Pattern
|
||||
|
||||
For HTMX endpoints that render partials:
|
||||
|
||||
```python
|
||||
# views.py
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
def scenario_row(request, pk):
|
||||
"""Render single scenario row partial for HTMX."""
|
||||
scenario = get_object_or_404(Scenario, pk=pk)
|
||||
# Syntax: "template.html#partial_name"
|
||||
return render(request, "scheduler/scenario_list.html#scenario_row", {
|
||||
"scenario": scenario
|
||||
})
|
||||
|
||||
@require_http_methods(["DELETE"])
|
||||
def scenario_delete(request, pk):
|
||||
"""Delete scenario and return empty response for HTMX swap."""
|
||||
scenario = get_object_or_404(Scenario, pk=pk)
|
||||
scenario.delete()
|
||||
# Return empty string - HTMX will remove the row
|
||||
return HttpResponse("")
|
||||
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def scenario_edit_header(request, pk):
|
||||
"""Inline edit scenario header via HTMX."""
|
||||
scenario = get_object_or_404(Scenario, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = ScenarioHeaderForm(request.POST, instance=scenario)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
# Return the display partial
|
||||
return render(request, "scheduler/scenario_detail.html#scenario_header", {
|
||||
"scenario": scenario
|
||||
})
|
||||
else:
|
||||
form = ScenarioHeaderForm(instance=scenario)
|
||||
|
||||
# Return edit form partial
|
||||
return render(request, "scheduler/scenario_edit_header.html", {
|
||||
"scenario": scenario,
|
||||
"form": form
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template Organization (league-planner)
|
||||
|
||||
```
|
||||
templates/
|
||||
├── base.html # Main base template
|
||||
├── includes/
|
||||
│ ├── navbar.html
|
||||
│ ├── footer.html
|
||||
│ ├── pagination.html
|
||||
│ └── modal.html
|
||||
├── scheduler/
|
||||
│ ├── scenario_list.html # With partials for rows
|
||||
│ ├── scenario_detail.html # With partials for sections
|
||||
│ ├── scenario_form.html # With form partial
|
||||
│ └── _scenario_row.html # Standalone partial (legacy)
|
||||
├── draws/
|
||||
│ └── ...
|
||||
└── qualifiers/
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Partial nicht gefunden**: Partial-Namen müssen exakt matchen, keine Leerzeichen
|
||||
- **Context fehlt**: Partials erben den Context - stelle sicher, dass Variablen verfügbar sind
|
||||
- **HTMX Swap-Probleme**: Verwende IDs für präzises Targeting (`hx-target="#element-id"`)
|
||||
- **CSP Violations**: Inline-Styles/Scripts brauchen `nonce="{{ csp_nonce }}"` wenn CSP aktiv
|
||||
- **N+1 in Templates**: Verwende `select_related`/`prefetch_related` in Views, nicht im Template
|
||||
- **forloop.length Performance**: Bei großen Listen kann dies teuer sein (zählt vorab)
|
||||
|
||||
## Deprecation Warning
|
||||
|
||||
⚠️ **urlize Filter**: Default-Protokoll wechselt von HTTP zu HTTPS in Django 7.0.
|
||||
Setze `URLIZE_ASSUME_HTTPS = True` in settings.py für Vorwärtskompatibilität.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
Test the template:
|
||||
|
||||
```bash
|
||||
# Check for template syntax errors
|
||||
python manage.py check --deploy
|
||||
|
||||
# Render template in shell
|
||||
python manage.py shell
|
||||
>>> from django.template.loader import render_to_string
|
||||
>>> html = render_to_string('scheduler/scenario_list.html', {'scenarios': []})
|
||||
>>> print(html)
|
||||
|
||||
# Test partial rendering
|
||||
>>> html = render_to_string('scheduler/scenario_list.html#scenario_row', {'scenario': scenario})
|
||||
```
|
||||
123
skills/qualifiers-model/SKILL.md
Normal file
123
skills/qualifiers-model/SKILL.md
Normal file
@ -0,0 +1,123 @@
|
||||
# qualifiers-model
|
||||
|
||||
Erstellt und erweitert Django Models für das qualifiers-Modul (UEFA Qualifikationsturniere).
|
||||
|
||||
## Trigger
|
||||
|
||||
- Neue QNode, QMatch, QGame, QGrouping Models
|
||||
- Model-Erweiterungen für Qualifiers
|
||||
- "qualifiers model", "qnode", "qmatch", "qgame"
|
||||
|
||||
## Kontext
|
||||
|
||||
Das qualifiers-Modul verwaltet UEFA-Qualifikationsturniere mit folgender Hierarchie:
|
||||
- **QPath** → QTier → QNode (Turnier-Struktur)
|
||||
- **QNode** → QGrouping → QPosition (Gruppenbildung)
|
||||
- **QNode** → QMatch → QGame (Spielpaarungen)
|
||||
|
||||
## Regeln
|
||||
|
||||
1. **Immer db_index für ForeignKeys setzen** wenn häufig gefiltert wird:
|
||||
```python
|
||||
scenario = models.ForeignKey('scheduler.Scenario', on_delete=models.CASCADE, db_index=True)
|
||||
```
|
||||
|
||||
2. **Meta-Klasse mit indexes und constraints**:
|
||||
```python
|
||||
class Meta:
|
||||
ordering = ['-tier__ypos', '-stage__xpos']
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['name', 'scenario'], name='unique_qnode_name_scenario')
|
||||
]
|
||||
indexes = [
|
||||
models.Index(fields=['scenario', 'tier', 'stage'], name='qnode_scenario_tier_stage_idx'),
|
||||
]
|
||||
```
|
||||
|
||||
3. **related_name konsistent benennen**:
|
||||
- Plural für ForeignKey: `related_name='groupings'`
|
||||
- Model-Name für M2M: `related_name='qnodes_seeded'`
|
||||
|
||||
4. **State-Machine Pattern für QNode**:
|
||||
```python
|
||||
NODESTATE_CHOICES = (
|
||||
(0, "none"), # Warten auf Vorgänger
|
||||
(1, "seeded"), # Teams zugeordnet
|
||||
(2, "grouping"), # Gruppen gebildet
|
||||
(3, "draw"), # Spiele ausgelost
|
||||
(4, "finalized"), # Alle Ergebnisse da
|
||||
)
|
||||
```
|
||||
|
||||
5. **GameMixin für QGame verwenden**:
|
||||
```python
|
||||
from scheduler.models import GameMixin
|
||||
|
||||
class QGame(GameMixin):
|
||||
# Erbt: homeGoals, awayGoals, resultEntered, season
|
||||
qnode = models.ForeignKey(QNode, ...)
|
||||
```
|
||||
|
||||
6. **Signals mit transaction.atomic()**:
|
||||
```python
|
||||
@receiver(post_save, sender=QGame)
|
||||
def post_match_signal(sender, instance, created, **kwargs):
|
||||
with transaction.atomic():
|
||||
# Signal-Logik
|
||||
```
|
||||
|
||||
## Beispiel: Neues Feld zu QNode
|
||||
|
||||
```python
|
||||
# qualifiers/models.py
|
||||
class QNode(models.Model):
|
||||
# Existierende Felder...
|
||||
|
||||
# Neues Feld
|
||||
auto_advance = models.BooleanField(
|
||||
default=False,
|
||||
help_text="Automatisch zum nächsten State wechseln"
|
||||
)
|
||||
```
|
||||
|
||||
## Beispiel: Neues Model
|
||||
|
||||
```python
|
||||
# qualifiers/models.py
|
||||
class QNodeViolation(models.Model):
|
||||
"""Speichert Constraint-Verletzungen für einen Node."""
|
||||
node = models.ForeignKey(
|
||||
QNode,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='violations',
|
||||
db_index=True
|
||||
)
|
||||
scenario = models.ForeignKey(
|
||||
'scheduler.Scenario',
|
||||
on_delete=models.CASCADE,
|
||||
db_index=True
|
||||
)
|
||||
type = models.CharField(max_length=50) # 'country_clash', 'distance', etc.
|
||||
message = models.TextField()
|
||||
severity = models.IntegerField(default=1) # 1=Warning, 2=Error
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['node', 'scenario'], name='qnodeviolation_node_scen_idx'),
|
||||
]
|
||||
```
|
||||
|
||||
## Migration erstellen
|
||||
|
||||
```bash
|
||||
conda activate planner
|
||||
python manage.py makemigrations qualifiers
|
||||
python manage.py migrate qualifiers
|
||||
```
|
||||
|
||||
## Referenz-Dateien
|
||||
|
||||
- [qualifiers/models.py](qualifiers/models.py) - Alle Models
|
||||
- [docs/qualifiers/models/](docs/qualifiers/models/) - Dokumentation
|
||||
163
skills/qualifiers-seeding/SKILL.md
Normal file
163
skills/qualifiers-seeding/SKILL.md
Normal file
@ -0,0 +1,163 @@
|
||||
# qualifiers-seeding
|
||||
|
||||
Automatisiert das Team-Seeding für UEFA-Qualifikationsturniere.
|
||||
|
||||
## Trigger
|
||||
|
||||
- Neue Saison erstellen
|
||||
- Team-Import
|
||||
- "seeding", "seed teams", "neue saison"
|
||||
|
||||
## Kontext
|
||||
|
||||
Das Seeding erstellt die Node-Struktur und weist Teams basierend auf UEFA-Koeffizienten zu. Die Hauptfunktion ist `seed_nodes25()` in [qualifiers/helpers.py](qualifiers/helpers.py).
|
||||
|
||||
## Regeln
|
||||
|
||||
1. **Node-Struktur folgt UEFA-Format**:
|
||||
```
|
||||
Champions Path:
|
||||
└── UCL: Q1 → Q2 → Q3 → PO
|
||||
└── UEL: Q2 → Q3 → PO
|
||||
└── UECL: Q3 → PO
|
||||
|
||||
League Path:
|
||||
└── UCL: Q2 → Q3 → PO
|
||||
└── UEL: Q3 → PO
|
||||
```
|
||||
|
||||
2. **Koeffizienten-Ranking**:
|
||||
```python
|
||||
# Formel: Koeffizient * 100000 - Position
|
||||
# Höherer Wert = besseres Ranking
|
||||
coefficients[team.id] = float(100000 * team.coefficient - team.position)
|
||||
```
|
||||
|
||||
3. **State-Transitions**:
|
||||
- `none` (0) → `seeded` (1): Nach `seeded_teams.set()`
|
||||
- Vorgänger-Nodes müssen mindestens State `draw` (3) haben
|
||||
|
||||
4. **GroupsOfSize automatisch erstellen**:
|
||||
```python
|
||||
node.create_groupsofsize()
|
||||
# Erstellt: 4, 6, 8, 10, 12, 20 mit number=0
|
||||
```
|
||||
|
||||
## Beispiel: Nodes für neue Saison erstellen
|
||||
|
||||
```python
|
||||
from qualifiers.models import QPath, QTier, QStage, QNode
|
||||
|
||||
# Path erstellen
|
||||
cp = QPath.objects.create(scenario=scenario, name='Champions Path')
|
||||
|
||||
# Tiers erstellen
|
||||
ucl = QTier.objects.create(
|
||||
scenario=scenario,
|
||||
path=cp,
|
||||
name='UCL',
|
||||
ypos=3,
|
||||
optimize_prio=50
|
||||
)
|
||||
|
||||
# Stages erstellen
|
||||
q1 = QStage.objects.create(scenario=scenario, path=cp, name='Q1', xpos=1)
|
||||
q2 = QStage.objects.create(scenario=scenario, path=cp, name='Q2', xpos=2)
|
||||
|
||||
# Nodes erstellen mit Navigation
|
||||
node_q1 = QNode.objects.create(
|
||||
scenario=scenario,
|
||||
name='Q1',
|
||||
tier=ucl,
|
||||
stage=q1,
|
||||
type=0, # Grouping
|
||||
state=1, # Seeded
|
||||
)
|
||||
|
||||
node_q2 = QNode.objects.create(
|
||||
scenario=scenario,
|
||||
name='Q2',
|
||||
tier=ucl,
|
||||
stage=q2,
|
||||
type=0,
|
||||
state=0, # Wartet auf Q1
|
||||
)
|
||||
|
||||
# Navigation setzen
|
||||
node_q1.winners = node_q2
|
||||
node_q1.save()
|
||||
```
|
||||
|
||||
## Beispiel: Teams seeden
|
||||
|
||||
```python
|
||||
from scheduler.models import Team
|
||||
from qualifiers.models import QNode
|
||||
|
||||
node = QNode.objects.get(
|
||||
scenario=scenario,
|
||||
tier__name='UCL',
|
||||
stage__name='Q1'
|
||||
)
|
||||
|
||||
# Top 16 Teams nach Koeffizient
|
||||
teams = Team.objects.filter(
|
||||
season=scenario.season,
|
||||
active=True
|
||||
).order_by('-coefficient')[:16]
|
||||
|
||||
# Teams zuweisen
|
||||
node.seeded_teams.set(teams)
|
||||
node.state = 1 # Seeded
|
||||
node.save()
|
||||
|
||||
# Gruppengrößen erstellen
|
||||
node.create_groupsofsize()
|
||||
```
|
||||
|
||||
## Beispiel: Upcomers von Vorgänger-Node
|
||||
|
||||
```python
|
||||
# Nach Q1 Draw: Teams für Q2 sammeln
|
||||
q1_node = QNode.objects.get(scenario=scenario, tier__name='UCL', stage__name='Q1')
|
||||
q2_node = QNode.objects.get(scenario=scenario, tier__name='UCL', stage__name='Q2')
|
||||
|
||||
# Upcomers sind finalisierte Matches aus Q1
|
||||
upcomers = q2_node.upcomers(scenario)
|
||||
|
||||
# Format:
|
||||
# [
|
||||
# {'id': 123, 'team': <Team>, 'coeff': 45.5, 'countries': ['GER'], ...},
|
||||
# {'id': 456, 'match': <QMatch>, 'coeff': 30.2, 'countries': ['ESP', 'ITA'], ...},
|
||||
# ]
|
||||
```
|
||||
|
||||
## Validierung
|
||||
|
||||
```python
|
||||
def validate_node_seeding(node):
|
||||
"""Prüft Seeding-Voraussetzungen."""
|
||||
errors = []
|
||||
|
||||
# Mindestens 2 Teams
|
||||
total_teams = node.nTeams(node.upcomers(node.scenario))
|
||||
if total_teams < 2:
|
||||
errors.append("Mindestens 2 Teams erforderlich")
|
||||
|
||||
# Gerade Anzahl
|
||||
if total_teams % 2 != 0:
|
||||
errors.append("Ungerade Teamanzahl")
|
||||
|
||||
# Alle Vorgänger finalisiert
|
||||
for pred in node.qnode_winners.all() | node.qnode_losers.all():
|
||||
if pred.current_state(node.scenario) < 3:
|
||||
errors.append(f"Vorgänger {pred.which()} nicht im Draw-State")
|
||||
|
||||
return errors
|
||||
```
|
||||
|
||||
## Referenz-Dateien
|
||||
|
||||
- [qualifiers/helpers.py](qualifiers/helpers.py) - seed_nodes25()
|
||||
- [qualifiers/uefadigitalapi/seed_2025.py](qualifiers/uefadigitalapi/seed_2025.py)
|
||||
- [docs/qualifiers/workflows/seeding.md](docs/qualifiers/workflows/seeding.md)
|
||||
138
skills/qualifiers-solver/SKILL.md
Normal file
138
skills/qualifiers-solver/SKILL.md
Normal file
@ -0,0 +1,138 @@
|
||||
# qualifiers-solver
|
||||
|
||||
Erstellt und optimiert MIP-Modelle für die Gruppenbildung in UEFA-Qualifikationsturnieren.
|
||||
|
||||
## Trigger
|
||||
|
||||
- Neue Optimierungsregeln
|
||||
- Solver-Constraints
|
||||
- "grouping solver", "mip constraint", "optimization"
|
||||
|
||||
## Kontext
|
||||
|
||||
Der Solver verwendet PuLP mit Xpress/CBC Backend zur Gruppenbildung. Hauptfunktion ist `groupTeams()` in [qualifiers/solver/draws.py](qualifiers/solver/draws.py).
|
||||
|
||||
## Regeln
|
||||
|
||||
1. **Solver-Auswahl über Environment**:
|
||||
```python
|
||||
from leagues.settings import SOLVER
|
||||
|
||||
if SOLVER == 'xpress':
|
||||
solver = XPRESS_PY(msg=1, mipgap=mipgap)
|
||||
else:
|
||||
solver = PULP_CBC_CMD(msg=1, mipGap=mipgap)
|
||||
```
|
||||
|
||||
2. **Entscheidungsvariablen als Dict**:
|
||||
```python
|
||||
# Team t in Gruppe g
|
||||
x = pulp.LpVariable.dicts(
|
||||
"x",
|
||||
[(t, g) for t in teams for g in groups],
|
||||
cat='Binary'
|
||||
)
|
||||
```
|
||||
|
||||
3. **Constraints mit lpSum**:
|
||||
```python
|
||||
from pulp import lpSum
|
||||
|
||||
# Jedes Team genau einer Gruppe zuordnen
|
||||
for t in teams:
|
||||
model += lpSum(x[t, g] for g in groups) == 1
|
||||
```
|
||||
|
||||
4. **Zielfunktion mit Gewichtung**:
|
||||
```python
|
||||
# priority: 0-100 (Distanz vs. Koeffizient)
|
||||
coeff_weight = priority / 100
|
||||
dist_weight = 1 - coeff_weight
|
||||
|
||||
model += coeff_weight * coeff_deviation + dist_weight * total_distance
|
||||
```
|
||||
|
||||
5. **Infeasibility-Handling**:
|
||||
```python
|
||||
status = model.solve(solver)
|
||||
if status != pulp.LpStatusOptimal:
|
||||
# IIS (Irreducible Infeasible Set) analysieren
|
||||
raise InfeasibleError(f"Solver status: {pulp.LpStatus[status]}")
|
||||
```
|
||||
|
||||
## Beispiel: Neuer Constraint
|
||||
|
||||
```python
|
||||
# qualifiers/solver/draws.py - in groupTeams()
|
||||
|
||||
# Beispiel: Maximal 2 Teams aus Top-10 Ländern pro Gruppe
|
||||
top10_countries = ['ESP', 'ENG', 'GER', 'ITA', 'FRA', 'POR', 'NED', 'BEL', 'UKR', 'TUR']
|
||||
|
||||
for g in groups:
|
||||
model += lpSum(
|
||||
x[t, g]
|
||||
for t in teams
|
||||
if any(c in top10_countries for c in t_countries[t])
|
||||
) <= 2, f"max_top10_in_group_{g}"
|
||||
```
|
||||
|
||||
## Beispiel: Soft Constraint (Zielfunktion)
|
||||
|
||||
```python
|
||||
# Strafe für ungleiche Gruppengrößen
|
||||
size_penalty = pulp.LpVariable.dicts(
|
||||
"size_penalty",
|
||||
groups,
|
||||
lowBound=0,
|
||||
cat='Continuous'
|
||||
)
|
||||
|
||||
avg_size = sum(groupsizes_all.values()) / len(groups)
|
||||
for g in groups:
|
||||
model += size_penalty[g] >= lpSum(x[t, g] for t in teams) - avg_size
|
||||
model += size_penalty[g] >= avg_size - lpSum(x[t, g] for t in teams)
|
||||
|
||||
# Zur Zielfunktion addieren
|
||||
model += ... + 0.1 * lpSum(size_penalty[g] for g in groups)
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
```python
|
||||
# Verbose Output
|
||||
model.solve(XPRESS_PY(msg=1))
|
||||
|
||||
# Variablenwerte ausgeben
|
||||
for v in model.variables():
|
||||
if v.varValue > 0.5:
|
||||
print(f"{v.name} = {v.varValue}")
|
||||
|
||||
# Constraint-Slack prüfen
|
||||
for name, constraint in model.constraints.items():
|
||||
if constraint.slack < 0:
|
||||
print(f"Verletzt: {name}, slack={constraint.slack}")
|
||||
```
|
||||
|
||||
## Performance-Tipps
|
||||
|
||||
1. **Symmetrie-Breaking**: Erste Gruppe fixieren
|
||||
```python
|
||||
# Erstes Team immer in Gruppe 0
|
||||
model += x[teams[0], 0] == 1
|
||||
```
|
||||
|
||||
2. **MIP-Gap setzen**:
|
||||
```python
|
||||
solver = XPRESS_PY(mipgap=0.01) # 1% Toleranz
|
||||
```
|
||||
|
||||
3. **Warm-Start mit fixed_groups**:
|
||||
```python
|
||||
for (team_id, group_id) in fixed_groups:
|
||||
model += x[team_id, group_id] == 1
|
||||
```
|
||||
|
||||
## Referenz-Dateien
|
||||
|
||||
- [qualifiers/solver/draws.py](qualifiers/solver/draws.py) - Solver-Code
|
||||
- [docs/qualifiers/solver/](docs/qualifiers/solver/) - Dokumentation
|
||||
184
skills/qualifiers-uefa-api/SKILL.md
Normal file
184
skills/qualifiers-uefa-api/SKILL.md
Normal file
@ -0,0 +1,184 @@
|
||||
# qualifiers-uefa-api
|
||||
|
||||
Integriert die UEFA Digital API für Team-Daten, Koeffizienten und Clashes.
|
||||
|
||||
## Trigger
|
||||
|
||||
- UEFA API-Aufrufe
|
||||
- Team-Synchronisation
|
||||
- Clash-Updates
|
||||
- "uefa api", "team sync", "clashes aktualisieren"
|
||||
|
||||
## Kontext
|
||||
|
||||
Die UEFA Digital API verwendet OAuth2 Client Credentials Flow. API-Skripte befinden sich in [qualifiers/uefadigitalapi/](qualifiers/uefadigitalapi/).
|
||||
|
||||
## Regeln
|
||||
|
||||
1. **Umgebungsvariablen für Credentials**:
|
||||
```bash
|
||||
UEFA_OAUTH_TENANT_ID=<azure-tenant-id>
|
||||
UEFA_OAUTH_CLIENT_ID=<client-id>
|
||||
UEFA_OAUTH_CLIENT_SECRET=<client-secret>
|
||||
UEFA_OAUTH_SCOPE=<api-scope>
|
||||
UEFA_SUBSCRIPTION_KEY=<subscription-key>
|
||||
UEFA_SUBSCRIPTION_KEY_TOKEN=<subscription-key-for-token>
|
||||
```
|
||||
|
||||
2. **Token-Abruf**:
|
||||
```python
|
||||
import requests
|
||||
import os
|
||||
|
||||
def get_uefa_token():
|
||||
tenant_id = os.getenv("UEFA_OAUTH_TENANT_ID")
|
||||
response = requests.post(
|
||||
f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token",
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
data={
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": os.getenv("UEFA_OAUTH_CLIENT_ID"),
|
||||
"client_secret": os.getenv("UEFA_OAUTH_CLIENT_SECRET"),
|
||||
"scope": os.getenv("UEFA_OAUTH_SCOPE"),
|
||||
},
|
||||
)
|
||||
return response.json()['access_token']
|
||||
```
|
||||
|
||||
3. **API-Aufruf mit Bearer Token**:
|
||||
```python
|
||||
token = get_uefa_token()
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Ocp-Apim-Subscription-Key": os.getenv("UEFA_SUBSCRIPTION_KEY_TOKEN"),
|
||||
}
|
||||
|
||||
response = requests.get(
|
||||
"https://api.digital.uefa.com/comp/v2/...",
|
||||
headers=headers,
|
||||
timeout=30
|
||||
)
|
||||
```
|
||||
|
||||
4. **Retry-Logik bei Fehlern**:
|
||||
```python
|
||||
import time
|
||||
|
||||
def safe_api_call(url, headers, retries=3):
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=30)
|
||||
if response.status_code == 429: # Rate limit
|
||||
retry_after = int(response.headers.get('Retry-After', 60))
|
||||
time.sleep(retry_after)
|
||||
continue
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
if attempt == retries - 1:
|
||||
raise
|
||||
time.sleep(2 ** attempt) # Exponential backoff
|
||||
```
|
||||
|
||||
5. **Daten-Mapping**:
|
||||
| UEFA Feld | Django Model | Feld |
|
||||
|-----------|--------------|------|
|
||||
| `teamId` | Team | `external_id` |
|
||||
| `teamName` | Team | `name` |
|
||||
| `countryCode` | Team.countryObj | `shortname` |
|
||||
| `coefficient` | Team | `coefficient` |
|
||||
| `position` | Team | `position` |
|
||||
|
||||
## Beispiel: Teams importieren
|
||||
|
||||
```python
|
||||
import requests
|
||||
from scheduler.models import Team, Country
|
||||
|
||||
def import_teams(scenario, competition_id):
|
||||
token = get_uefa_token()
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Ocp-Apim-Subscription-Key": os.getenv("UEFA_SUBSCRIPTION_KEY_TOKEN"),
|
||||
}
|
||||
|
||||
url = f"https://api.digital.uefa.com/comp/v2/competitions/{competition_id}/teams"
|
||||
data = safe_api_call(url, headers)
|
||||
|
||||
for team_data in data['teams']:
|
||||
country = Country.objects.get(shortname=team_data['countryCode'])
|
||||
|
||||
team, created = Team.objects.update_or_create(
|
||||
season=scenario.season,
|
||||
external_id=team_data['teamId'],
|
||||
defaults={
|
||||
'name': team_data['teamName'],
|
||||
'countryObj': country,
|
||||
'coefficient': team_data.get('coefficient', 0),
|
||||
'position': team_data.get('position', 0),
|
||||
'active': True,
|
||||
}
|
||||
)
|
||||
|
||||
if created:
|
||||
print(f"Neues Team: {team.name}")
|
||||
```
|
||||
|
||||
## Beispiel: Clashes aktualisieren
|
||||
|
||||
```python
|
||||
from qualifiers.models import QClash
|
||||
|
||||
def update_clashes(scenario, season_code):
|
||||
token = get_uefa_token()
|
||||
headers = {...}
|
||||
|
||||
url = f"https://api.digital.uefa.com/comp/v2/seasons/{season_code}/clashes"
|
||||
data = safe_api_call(url, headers)
|
||||
|
||||
for clash_data in data['clashes']:
|
||||
team1 = Team.objects.get(
|
||||
season=scenario.season,
|
||||
external_id=clash_data['team1Id']
|
||||
)
|
||||
team2 = Team.objects.get(
|
||||
season=scenario.season,
|
||||
external_id=clash_data['team2Id']
|
||||
)
|
||||
|
||||
QClash.objects.update_or_create(
|
||||
scenario=scenario,
|
||||
team1=team1,
|
||||
team2=team2,
|
||||
defaults={
|
||||
'type': clash_data.get('type', 0),
|
||||
'active_q1': True,
|
||||
'active_q2': True,
|
||||
'active_q3': True,
|
||||
'active_po': True,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Caching (geplant)
|
||||
|
||||
```python
|
||||
from django.core.cache import cache
|
||||
|
||||
def get_cached_api_data(key, url, headers, timeout=3600):
|
||||
"""Cached API-Aufruf."""
|
||||
cached = cache.get(key)
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
data = safe_api_call(url, headers)
|
||||
cache.set(key, data, timeout)
|
||||
return data
|
||||
```
|
||||
|
||||
## Referenz-Dateien
|
||||
|
||||
- [qualifiers/uefadigitalapi/uefa_api_2526.py](qualifiers/uefadigitalapi/uefa_api_2526.py)
|
||||
- [qualifiers/uefadigitalapi/seed_2025.py](qualifiers/uefadigitalapi/seed_2025.py)
|
||||
- [qualifiers/uefadigitalapi/update_clashes.py](qualifiers/uefadigitalapi/update_clashes.py)
|
||||
- [docs/qualifiers/api/uefa-oauth2.md](docs/qualifiers/api/uefa-oauth2.md)
|
||||
117
skills/translation-review/SKILL.md
Normal file
117
skills/translation-review/SKILL.md
Normal file
@ -0,0 +1,117 @@
|
||||
---
|
||||
name: translation-review
|
||||
description: Überprüft Django .po Übersetzungsdateien auf Qualität, Kontext-Passgenauigkeit, Konsistenz und fehlende Übersetzungen. Verbessert Übersetzungen direkt. Für Translation-QA und Review.
|
||||
argument-hint: "[locale] [--dry-run]"
|
||||
allowed-tools: Glob, Grep, Read, Edit, Write
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# Django Translation Review
|
||||
|
||||
Überprüft und verbessert Django `.po` Übersetzungsdateien im league-planner Projekt. Stellt sicher, dass Übersetzungen kontextgerecht, konsistent und vollständig sind.
|
||||
|
||||
## Argumente
|
||||
|
||||
- `locale` (optional): Bestimmte Sprache prüfen, z.B. `de`, `fr`, `ko`. Ohne Angabe → alle Locales.
|
||||
- `--dry-run` (optional): Nur Bericht erstellen, keine Änderungen vornehmen.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 1: Discovery
|
||||
|
||||
1. Finde alle `.po` Files mit Glob: `**/locale/*/LC_MESSAGES/django.po`
|
||||
2. Filtere nach dem angegebenen Locale (falls angegeben)
|
||||
3. Überspringe `locale/common/` (das ist ein Referenz-File, kein echtes Locale)
|
||||
4. Lies die `.po` Files und erstelle eine Übersicht: Locale, Anzahl Einträge, leere msgstr, fuzzy-Einträge
|
||||
|
||||
### Phase 2: Kontext-Analyse
|
||||
|
||||
Um kontextgerechte Übersetzungen sicherzustellen, lies diese Kontext-Quellen:
|
||||
|
||||
1. **`scheduler/choices.py`** — Enthält Sport-spezifische Choice-Tuples mit `gettext_lazy`. Hier ist der Fachkontext (Sport/Liga-Planung) am klarsten.
|
||||
2. **Source-Referenzen in .po** — Jeder `msgid` hat `#:` Kommentare die zeigen, wo der String verwendet wird. Nutze diese um den Kontext zu verstehen:
|
||||
- Strings aus `templates/` → UI-Labels, Buttons, Überschriften
|
||||
- Strings aus `models.py` → Feld-Labels, verbose_names
|
||||
- Strings aus `choices.py` → Dropdown-/Select-Optionen
|
||||
- Strings aus `views.py` → Nachrichten, Fehlermeldungen
|
||||
- Strings aus `helpers.py` → Solver-/System-Meldungen
|
||||
|
||||
### Phase 3: Qualitätsprüfung
|
||||
|
||||
Prüfe jede Übersetzung auf diese Kriterien:
|
||||
|
||||
#### Kritisch
|
||||
- **Leere Übersetzungen**: `msgstr ""` wo eine Übersetzung erwartet wird
|
||||
- **Fuzzy-Markierungen**: `#, fuzzy` Einträge die manuell bestätigt werden müssen
|
||||
- **Falsche Platzhalter**: `%(name)s`, `{0}`, `%d` müssen exakt übereinstimmen
|
||||
|
||||
#### Kontext
|
||||
- **Sport-Fachbegriffe**: Im Kontext von Liga-Planung die richtigen Begriffe verwenden:
|
||||
- "Match" → "Spiel" (nicht "Übereinstimmung")
|
||||
- "Day" (als Spieltag) → "Spieltag" (nicht "Tag")
|
||||
- "Round" → "Runde" oder "Spieltag" je nach Kontext
|
||||
- "Home/Away" → "Heim/Auswärts"
|
||||
- "Fixture" → "Begegnung" oder "Ansetzung"
|
||||
- "Draw" → "Auslosung" (nicht "Zeichnung")
|
||||
- "Group" (im Turnier) → "Gruppe"
|
||||
- "Seed" → "Setzliste"/"gesetzt" (nicht "Samen")
|
||||
- "Clash" → "Überschneidung" oder "Konflikt"
|
||||
- "Scenario" → "Szenario"
|
||||
- "Constraint" → "Nebenbedingung" oder "Einschränkung"
|
||||
- "Wish" → "Wunsch" (Planungswunsch)
|
||||
- "Run" (Solver-Run) → "Lauf" oder "Durchlauf"
|
||||
- "Slot" → "Zeitfenster" oder "Slot"
|
||||
|
||||
#### Konsistenz
|
||||
- **Gleiche Begriffe gleich übersetzen**: Wenn "Scenario" einmal "Szenario" ist, muss es überall "Szenario" sein
|
||||
- **Stil-Konsistenz**: Formell/Informell einheitlich (Du vs. Sie). league-planner verwendet **Du-Form**
|
||||
- **Groß-/Kleinschreibung**: Deutsche Substantive groß, UI-Labels konsistent
|
||||
|
||||
#### Stil
|
||||
- **Natürliche Sprache**: Übersetzungen sollen sich natürlich anfühlen, nicht wie maschinelle Übersetzung
|
||||
- **Kürze**: UI-Labels kurz halten, keine unnötigen Wörter
|
||||
- **Aktive Sprache**: "Szenario wurde erstellt" statt "Es wurde ein Szenario erstellt"
|
||||
|
||||
### Phase 4: Änderungen anwenden
|
||||
|
||||
Wenn NICHT `--dry-run`:
|
||||
|
||||
1. Für jede gefundene Verbesserung, nutze das Edit-Tool um den `msgstr` zu aktualisieren
|
||||
2. Entferne `#, fuzzy` Markierungen wenn die Übersetzung korrekt bestätigt wurde
|
||||
3. Fülle leere `msgstr` aus, sofern der Kontext eine sichere Übersetzung erlaubt
|
||||
4. **WICHTIG**: Ändere NIEMALS `msgid` — nur `msgstr` darf geändert werden
|
||||
5. **WICHTIG**: Erhalte alle `#:` Source-Referenz-Kommentare unverändert
|
||||
6. **WICHTIG**: Erhalte Python-Format-Strings exakt (`%(name)s`, `{0}`, `%d` etc.)
|
||||
|
||||
### Phase 5: Zusammenfassung
|
||||
|
||||
Gib am Ende eine strukturierte Zusammenfassung:
|
||||
|
||||
```
|
||||
## Translation Review — [Locale]
|
||||
|
||||
### Statistik
|
||||
- Geprüfte Einträge: X
|
||||
- Verbesserungen: Y
|
||||
- Fehlende Übersetzungen (gefüllt): Z
|
||||
- Fuzzy aufgelöst: W
|
||||
- Unverändert: V
|
||||
|
||||
### Änderungen
|
||||
| # | msgid | Alt | Neu | Grund |
|
||||
|---|-------|-----|-----|-------|
|
||||
| 1 | "..." | "..." | "..." | Kontext/Konsistenz/Stil |
|
||||
|
||||
### Offene Punkte
|
||||
- [Einträge die manuelle Prüfung brauchen]
|
||||
```
|
||||
|
||||
## Wichtige Regeln
|
||||
|
||||
- **Konservativ sein**: Im Zweifel lieber nicht ändern als eine falsche Übersetzung einführen
|
||||
- **Kontext prüfen**: Immer die `#:` Source-Referenzen lesen bevor eine Übersetzung geändert wird
|
||||
- **Format-Strings erhalten**: `%(count)d Spiele` nicht `%(anzahl)d Spiele`
|
||||
- **Plural-Forms beachten**: `msgid_plural` / `msgstr[0]` / `msgstr[1]` korrekt handhaben
|
||||
- **Kein Overengineering**: Wenn eine Übersetzung gut genug ist, nicht ändern nur um sie "schöner" zu machen
|
||||
- **Locale `en`**: Englische .po Files haben typischerweise leere msgstr — das ist korrekt, da `msgid` bereits Englisch ist. Diese NICHT ausfüllen.
|
||||
- **Locale `common`**: Ist ein Referenz-File, nicht bearbeiten
|
||||
Loading…
x
Reference in New Issue
Block a user