diff --git a/.github/workflows/i18n.yml b/.github/workflows/i18n.yml new file mode 100644 index 0000000..7862e91 --- /dev/null +++ b/.github/workflows/i18n.yml @@ -0,0 +1,33 @@ +name: i18n + +on: + push: + pull_request: + +jobs: + compile-translations: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: backend + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install gettext + run: | + sudo apt-get update + sudo apt-get install -y gettext + + - name: Install Python dependencies + run: pip install -r requirements.txt + + - name: Compile translations + run: django-admin compilemessages diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..269a172 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +COMPOSE ?= docker compose + +.PHONY: i18n-extract-en i18n-extract-de i18n-compile i18n-update-en i18n-update-de + +i18n-extract-en: + $(COMPOSE) exec -T web django-admin makemessages -l en + +i18n-extract-de: + $(COMPOSE) exec -T web django-admin makemessages -l de + +i18n-compile: + $(COMPOSE) exec -T web django-admin compilemessages + +i18n-update-en: i18n-extract-en i18n-compile + +i18n-update-de: i18n-extract-de i18n-compile diff --git a/README.md b/README.md index cb652b8..b191e5e 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,21 @@ This project now uses Django's standard i18n workflow for long-term maintainabil - `docker compose exec -T web django-admin compilemessages` - Add more languages the same way: - `docker compose exec -T web django-admin makemessages -l de` +- Convenience targets: + - `make i18n-update-en` + - `make i18n-update-de` + - `make i18n-compile` Notes: - `gettext` is installed in the Docker image, so `compilemessages` works inside the container. - Translation files live under `backend/locale/`. -- Core fixed UI is bilingual now; dynamic builder content and most PDF/email business text are not fully bilingual yet. +- Core fixed UI is bilingual now. +- Dynamic builder-driven content is now bilingual for: + - Form Builder option labels + - Form Builder field label/help-text overrides + - Intro Builder checklist item labels +- Most email template business text and several generated PDF text blocks are still not fully bilingual yet. +- CI now validates that translation catalogs compile successfully on push and pull request. ## Current implemented scope - Onboarding form with labels mapped from your CSV schema. diff --git a/backend/workflows/admin.py b/backend/workflows/admin.py index 62ebabc..ce04659 100644 --- a/backend/workflows/admin.py +++ b/backend/workflows/admin.py @@ -28,9 +28,9 @@ class OffboardingRequestAdmin(admin.ModelAdmin): @admin.register(FormOption) class FormOptionAdmin(admin.ModelAdmin): - list_display = ('category', 'label', 'value', 'sort_order', 'is_active') + list_display = ('category', 'label', 'label_en', 'value', 'sort_order', 'is_active') list_filter = ('category', 'is_active') - search_fields = ('label', 'value') + search_fields = ('label', 'label_en', 'value') ordering = ('category', 'sort_order', 'label') @@ -38,16 +38,16 @@ class FormOptionAdmin(admin.ModelAdmin): class FormFieldConfigAdmin(admin.ModelAdmin): list_display = ('form_type', 'field_name', 'page_key', 'sort_order', 'is_visible', 'is_required') list_filter = ('form_type', 'page_key', 'is_visible', 'is_required') - search_fields = ('field_name', 'label_override', 'help_text_override') + search_fields = ('field_name', 'label_override', 'label_override_en', 'help_text_override', 'help_text_override_en') ordering = ('form_type', 'sort_order', 'field_name') list_editable = ('page_key', 'sort_order', 'is_visible', 'is_required') @admin.register(IntroChecklistItem) class IntroChecklistItemAdmin(admin.ModelAdmin): - list_display = ('section', 'label', 'condition_field', 'condition_operator', 'condition_value', 'sort_order', 'is_active') + list_display = ('section', 'label', 'label_en', 'condition_field', 'condition_operator', 'condition_value', 'sort_order', 'is_active') list_filter = ('section', 'condition_operator', 'is_active') - search_fields = ('label', 'condition_field', 'condition_value') + search_fields = ('label', 'label_en', 'condition_field', 'condition_value') ordering = ('section', 'sort_order', 'label') list_editable = ('sort_order', 'is_active') diff --git a/backend/workflows/form_builder.py b/backend/workflows/form_builder.py index ef0f965..d7a03cf 100644 --- a/backend/workflows/form_builder.py +++ b/backend/workflows/form_builder.py @@ -1,4 +1,5 @@ from collections import OrderedDict +from django.utils.translation import get_language from .models import FormFieldConfig @@ -154,17 +155,20 @@ def apply_form_field_config(form_type: str, form) -> None: field_names = list(form.fields.keys()) configs = _ensure_configs(form_type, field_names) locked = LOCKED_FIELD_RULES.get(form_type, set()) + language_code = get_language() for field_name, field in list(form.fields.items()): cfg = configs.get(field_name) if not cfg: continue - if cfg.label_override.strip(): - field.label = cfg.label_override.strip() + translated_label = cfg.translated_label_override(language_code) + if translated_label: + field.label = translated_label - if cfg.help_text_override.strip(): - field.help_text = cfg.help_text_override.strip() + translated_help_text = cfg.translated_help_text_override(language_code) + if translated_help_text: + field.help_text = translated_help_text if field_name not in locked and cfg.is_required is not None: field.required = cfg.is_required diff --git a/backend/workflows/forms.py b/backend/workflows/forms.py index d8deee6..a40e1ff 100644 --- a/backend/workflows/forms.py +++ b/backend/workflows/forms.py @@ -1,5 +1,6 @@ from django import forms from pathlib import Path +from django.utils.translation import get_language from .form_builder import apply_form_field_config from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest @@ -214,7 +215,8 @@ class OnboardingRequestForm(forms.ModelForm): options = FormOption.objects.filter(category=category, is_active=True).order_by('sort_order', 'label') if not options.exists(): return fallback - return [(o.value or o.label, o.label) for o in options] + language_code = get_language() + return [(o.value or o.label, o.translated_label(language_code)) for o in options] def __init__(self, *args, **kwargs): self.requester_email = (kwargs.pop('requester_email', '') or '').strip().lower() diff --git a/backend/workflows/migrations/0029_formfieldconfig_help_text_override_en_and_more.py b/backend/workflows/migrations/0029_formfieldconfig_help_text_override_en_and_more.py new file mode 100644 index 0000000..671157e --- /dev/null +++ b/backend/workflows/migrations/0029_formfieldconfig_help_text_override_en_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.5 on 2026-03-24 10:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workflows', '0028_onboardingintroductionsession_exported_pdf_path'), + ] + + operations = [ + migrations.AddField( + model_name='formfieldconfig', + name='help_text_override_en', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='formfieldconfig', + name='label_override_en', + field=models.CharField(blank=True, max_length=255), + ), + migrations.AddField( + model_name='formoption', + name='label_en', + field=models.CharField(blank=True, max_length=255), + ), + migrations.AddField( + model_name='introchecklistitem', + name='label_en', + field=models.CharField(blank=True, max_length=255), + ), + ] diff --git a/backend/workflows/models.py b/backend/workflows/models.py index 77d1266..b5ef9b6 100644 --- a/backend/workflows/models.py +++ b/backend/workflows/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.utils.translation import get_language class EmployeeProfile(models.Model): @@ -98,6 +99,7 @@ class FormOption(models.Model): category = models.CharField(max_length=40, choices=CATEGORY_CHOICES) label = models.CharField(max_length=255) + label_en = models.CharField(max_length=255, blank=True) value = models.CharField(max_length=255, blank=True) sort_order = models.PositiveIntegerField(default=0) is_active = models.BooleanField(default=True) @@ -109,6 +111,12 @@ class FormOption(models.Model): def __str__(self) -> str: return f"{self.get_category_display()}: {self.label}" + def translated_label(self, language_code: str | None = None) -> str: + lang = (language_code or get_language() or 'de').split('-')[0] + if lang == 'en' and self.label_en.strip(): + return self.label_en.strip() + return self.label.strip() + class FormFieldConfig(models.Model): PAGE_CHOICES = [ @@ -130,7 +138,9 @@ class FormFieldConfig(models.Model): is_required = models.BooleanField(null=True, blank=True, default=None) page_key = models.CharField(max_length=20, blank=True, default='', choices=PAGE_CHOICES) label_override = models.CharField(max_length=255, blank=True) + label_override_en = models.CharField(max_length=255, blank=True) help_text_override = models.TextField(blank=True) + help_text_override_en = models.TextField(blank=True) class Meta: ordering = ['form_type', 'sort_order', 'field_name'] @@ -141,6 +151,18 @@ class FormFieldConfig(models.Model): def __str__(self) -> str: return f'{self.get_form_type_display()}: {self.field_name}' + def translated_label_override(self, language_code: str | None = None) -> str: + lang = (language_code or get_language() or 'de').split('-')[0] + if lang == 'en' and self.label_override_en.strip(): + return self.label_override_en.strip() + return self.label_override.strip() + + def translated_help_text_override(self, language_code: str | None = None) -> str: + lang = (language_code or get_language() or 'de').split('-')[0] + if lang == 'en' and self.help_text_override_en.strip(): + return self.help_text_override_en.strip() + return self.help_text_override.strip() + class NotificationTemplate(models.Model): TEMPLATE_CHOICES = [ @@ -249,6 +271,7 @@ class IntroChecklistItem(models.Model): section = models.CharField(max_length=30, choices=SECTION_CHOICES) label = models.CharField(max_length=255) + label_en = models.CharField(max_length=255, blank=True) sort_order = models.PositiveIntegerField(default=0) is_active = models.BooleanField(default=True) condition_field = models.CharField(max_length=80, blank=True) @@ -261,6 +284,12 @@ class IntroChecklistItem(models.Model): def __str__(self) -> str: return f'{self.get_section_display()}: {self.label}' + def translated_label(self, language_code: str | None = None) -> str: + lang = (language_code or get_language() or 'de').split('-')[0] + if lang == 'en' and self.label_en.strip(): + return self.label_en.strip() + return self.label.strip() + class OnboardingIntroductionSession(models.Model): STATUS_CHOICES = [ diff --git a/backend/workflows/static/workflows/css/form_builder.css b/backend/workflows/static/workflows/css/form_builder.css index 1e1a7c6..09b08db 100644 --- a/backend/workflows/static/workflows/css/form_builder.css +++ b/backend/workflows/static/workflows/css/form_builder.css @@ -235,7 +235,7 @@ body { .add-option-form { display: grid; - grid-template-columns: minmax(220px, 1fr) minmax(220px, 1fr) auto; + grid-template-columns: minmax(180px, 1fr) minmax(180px, 1fr) minmax(180px, 1fr) auto; gap: 8px; margin-bottom: 10px; } @@ -260,6 +260,7 @@ body { border: 1px solid #e2e8f0; padding: 7px 8px; text-align: left; + vertical-align: top; } .option-table th { @@ -300,6 +301,8 @@ body { .options-actions { margin-top: 10px; + display: flex; + justify-content: flex-end; } @media (max-width: 1120px) { diff --git a/backend/workflows/static/workflows/img/tubco-logo.svg b/backend/workflows/static/workflows/img/tubco-logo.svg new file mode 100644 index 0000000..d136a63 --- /dev/null +++ b/backend/workflows/static/workflows/img/tubco-logo.svg @@ -0,0 +1,13 @@ + + TUBCO + TUBCO wordmark + + + + TUBCO + + + TU BERLIN CORPORATE + + + diff --git a/backend/workflows/tasks.py b/backend/workflows/tasks.py index 5f36375..fd963be 100644 --- a/backend/workflows/tasks.py +++ b/backend/workflows/tasks.py @@ -8,6 +8,7 @@ from celery import shared_task from django.contrib.auth import get_user_model from django.conf import settings from django.utils import timezone +from django.utils.translation import gettext as _, get_language from jinja2 import Template from pypdf import PageObject, PdfReader, PdfWriter from xhtml2pdf import pisa @@ -294,7 +295,7 @@ def _matches_intro_condition(request_obj: OnboardingRequest, item: IntroChecklis return True -def _build_intro_sections_from_admin(request_obj: OnboardingRequest) -> dict[str, list[str]]: +def _build_intro_sections_from_admin(request_obj: OnboardingRequest, language_code: str | None = None) -> dict[str, list[str]]: items = list(IntroChecklistItem.objects.filter(is_active=True).order_by('section', 'sort_order', 'label')) if not items: return {} @@ -304,11 +305,26 @@ def _build_intro_sections_from_admin(request_obj: OnboardingRequest) -> dict[str if item.section not in section_map: continue if _matches_intro_condition(request_obj, item): - section_map[item.section].append(item.label) + section_map[item.section].append(item.translated_label(language_code)) return {key: values for key, values in section_map.items() if values} -def build_intro_sections_for_request(request_obj: OnboardingRequest) -> list[dict]: +def build_intro_sections_for_request(request_obj: OnboardingRequest, language_code: str | None = None) -> list[dict]: + lang = (language_code or get_language() or 'de').split('-')[0] + section_titles = { + 'de': { + 'workplace': 'Geräte und Arbeitsplatz', + 'accounts': 'Konten und Berechtigungen', + 'software': 'Software und Tools', + 'process': 'Prozesse und Hinweise', + }, + 'en': { + 'workplace': 'Devices and workplace', + 'accounts': 'Accounts and permissions', + 'software': 'Software and tools', + 'process': 'Processes and notes', + }, + } devices = _split_multiline(request_obj.needed_devices) software = _split_multiline(request_obj.needed_software) accesses = _split_multiline(request_obj.needed_accesses) @@ -354,12 +370,12 @@ def build_intro_sections_for_request(request_obj: OnboardingRequest) -> list[dic if request_obj.successor_name: process_items.append(f'Übergabe-/Nachfolgekontext besprochen: {request_obj.successor_name}') - custom_intro_items = _build_intro_sections_from_admin(request_obj) + custom_intro_items = _build_intro_sections_from_admin(request_obj, lang) intro_sections_raw = [ - ('workplace', 'Geräte und Arbeitsplatz', workplace_items), - ('accounts', 'Konten und Berechtigungen', account_items), - ('software', 'Software und Tools', software_items), - ('process', 'Prozesse und Hinweise', process_items), + ('workplace', section_titles.get(lang, section_titles['de'])['workplace'], workplace_items), + ('accounts', section_titles.get(lang, section_titles['de'])['accounts'], account_items), + ('software', section_titles.get(lang, section_titles['de'])['software'], software_items), + ('process', section_titles.get(lang, section_titles['de'])['process'], process_items), ] sections = [] @@ -714,7 +730,7 @@ def _generate_onboarding_pdf(request_obj: OnboardingRequest) -> Path: return output_pdf -def _generate_onboarding_intro_pdf(request_obj: OnboardingRequest) -> Path: +def _generate_onboarding_intro_pdf(request_obj: OnboardingRequest, language_code: str | None = None) -> Path: safe_name = _safe_filename_fragment(request_obj.full_name, fallback=f'onboarding_intro_{request_obj.id}') output_pdf = settings.PDF_OUTPUT_DIR / f'onboarding_intro_{safe_name}.pdf' temp_pdf = settings.PDF_OUTPUT_DIR / f'_temp_onboarding_intro_{safe_name}.pdf' @@ -729,7 +745,7 @@ def _generate_onboarding_intro_pdf(request_obj: OnboardingRequest) -> Path: 'title': section['title'], 'rows': _chunk_list([item['label'] for item in section['items']], chunk_size=2), } - for section in build_intro_sections_for_request(request_obj) + for section in build_intro_sections_for_request(request_obj, language_code=language_code) ] requester_email = request_obj.onboarded_by_email or '-' @@ -755,7 +771,11 @@ def _generate_onboarding_intro_pdf(request_obj: OnboardingRequest) -> Path: return output_pdf -def _generate_onboarding_intro_session_pdf(session: OnboardingIntroductionSession, admin_signature_name: str = '-') -> Path: +def _generate_onboarding_intro_session_pdf( + session: OnboardingIntroductionSession, + admin_signature_name: str = '-', + language_code: str | None = None, +) -> Path: request_obj = session.onboarding_request safe_name = _safe_filename_fragment(request_obj.full_name, fallback=f'onboarding_intro_session_{request_obj.id}') version = timezone.now().strftime('%Y%m%d%H%M%S') @@ -768,7 +788,7 @@ def _generate_onboarding_intro_session_pdf(session: OnboardingIntroductionSessio salutation = (request_obj.get_gender_display() or '').strip() display_name = f"{salutation} {request_obj.full_name}".strip() if salutation else request_obj.full_name - raw_sections = build_intro_sections_for_request(request_obj) + raw_sections = build_intro_sections_for_request(request_obj, language_code=language_code) checked_map = session.checklist_state or {} exported_sections = [] checked_count = 0 diff --git a/backend/workflows/templates/registration/login.html b/backend/workflows/templates/registration/login.html index df54158..e59e84c 100644 --- a/backend/workflows/templates/registration/login.html +++ b/backend/workflows/templates/registration/login.html @@ -1,7 +1,6 @@ {% load static i18n %} -{% get_current_language as CURRENT_LANGUAGE %} - + @@ -18,22 +17,13 @@ input { width: 100%; padding: 10px; box-sizing: border-box; border: 1px solid #cbd5e1; border-radius: 8px; } .btn { width: 100%; } .errorlist { color: #b91c1c; margin: 6px 0; } - .card-head { display:flex; justify-content:space-between; gap:12px; align-items:flex-start; } - .lang-switch { display:flex; gap:6px; } - .lang-btn { border:1px solid #d9e3f0; background:#f8fbff; color:#1f3a5f; border-radius:999px; padding:6px 10px; font-size:12px; font-weight:700; cursor:pointer; } - .lang-btn.active { background:#000078; border-color:#000078; color:#fff; } + .card-head { display:block; }
- -
- {% csrf_token %} - - - -
+

{% trans "Anmeldung" %}

{% trans "Bitte melden Sie sich mit Ihrem Benutzerkonto an." %}

diff --git a/backend/workflows/templates/workflows/developer_handbook.html b/backend/workflows/templates/workflows/developer_handbook.html new file mode 100644 index 0000000..dfe1d5f --- /dev/null +++ b/backend/workflows/templates/workflows/developer_handbook.html @@ -0,0 +1,223 @@ +{% load static %} + + + + + + Developer Handbook + + + + +
+ +
+

Developer Handbook

+
+ Project Wiki + Back to Home +
+
+

Engineering runbook for development, deployment, maintenance, and extension of the TUBCO Onboarding & Offboarding Portal.

+ + + +

1) Overview

+
+

This handbook is for developers and maintainers. It documents the actual engineering workflow of the standalone product repository.

+
    +
  • Repository: tubco-onboarding-offboarding-portal
  • +
  • Main stack: Django + Celery + PostgreSQL + Redis + MailHog
  • +
  • Runtime mode: Docker Compose for local development and staging-style operation
  • +
+
+ +

2) Repository Structure

+ + +

3) Local Development Workflow

+

Start

+
cd /Users/bostame/Documents/tubco-onboarding-offboarding-portal
+docker compose up -d --build
+

Main URLs

+ +

Bootstrap users

+ + +

4) Docker Operations

+
docker compose up -d --build
+docker compose restart web
+docker compose restart worker
+docker compose logs --no-color --tail=120 web
+docker compose logs --no-color --tail=120 worker
+docker compose down
+docker compose down -v
+
+ The source code is bind-mounted into the container. Most template/view/static changes only require a web restart, not a full rebuild. Image changes such as system packages require docker compose up -d --build. +
+ +

5) Database and Migrations

+
docker compose exec -T web python manage.py makemigrations
+docker compose exec -T web python manage.py migrate
+docker compose exec -T web python manage.py check
+ + +

6) Translation Workflow

+

Standard Django i18n path

+
make i18n-update-en
+make i18n-compile
+

Equivalent raw commands:

+
docker compose exec -T web django-admin makemessages -l en
+docker compose exec -T web django-admin compilemessages
+ + +

7) PDF Pipeline

+ +
+ xhtml2pdf is sensitive to layout complexity. Keep print templates conservative and verify every structural change with a real generated PDF. +
+ +

8) Email Pipeline

+ + +

9) Nextcloud Integration

+ + +

10) Builder Architecture

+

Form Builder

+ +

Intro Builder

+ +
+ Dynamic content should use explicit DE/EN fields with German fallback, not machine translation at runtime. +
+ +

11) Testing and Validation

+
docker compose exec -T web python manage.py check
+docker compose exec -T web python manage.py test
+docker compose exec -T web python manage.py run_staging_e2e_check
+ + +

12) Deployment and Release Checklist

+
    +
  1. Run manage.py check
  2. +
  3. Run tests or targeted verification
  4. +
  5. Run translation compile step
  6. +
  7. Generate at least one onboarding/offboarding PDF if PDF templates changed
  8. +
  9. Verify MailHog or SMTP path if email behavior changed
  10. +
  11. Verify Nextcloud upload if integration behavior changed
  12. +
  13. Update Project Wiki and Developer Handbook if architecture/workflow changed
  14. +
  15. Take a snapshot commit before major next-phase work
  16. +
+ +

13) Troubleshooting

+ + +

14) Security and Maintenance Notes

+ +
+ + diff --git a/backend/workflows/templates/workflows/form_builder.html b/backend/workflows/templates/workflows/form_builder.html index 65e48d0..6ac1d66 100644 --- a/backend/workflows/templates/workflows/form_builder.html +++ b/backend/workflows/templates/workflows/form_builder.html @@ -11,7 +11,7 @@
@@ -81,7 +81,8 @@ {% csrf_token %} - + + @@ -93,7 +94,8 @@ Sortierung - Label + Label (DE) + Label (EN) Value Aktiv Löschen @@ -107,6 +109,7 @@ ⋮⋮ + @@ -114,7 +117,7 @@ {% empty %} - Keine Optionen in dieser Kategorie. + Keine Optionen in dieser Kategorie. {% endfor %} @@ -124,6 +127,47 @@
+ +
+
+

Feldtexte verwalten

+
+
+ {% csrf_token %} +
+ + + + + + + + + + + + {% for item in field_text_items %} + + + + + + + + {% empty %} + + {% endfor %} + +
FeldLabel (DE)Label (EN)Hilfetext (DE)Hilfetext (EN)
+ + {{ item.field_name }} +
Keine Feldkonfigurationen verfügbar.
+
+
+ +
+
+
diff --git a/backend/workflows/templates/workflows/handbook.html b/backend/workflows/templates/workflows/handbook.html new file mode 100644 index 0000000..8ae3912 --- /dev/null +++ b/backend/workflows/templates/workflows/handbook.html @@ -0,0 +1,69 @@ +{% load static i18n %} + + + + + + Handbook + + + + +
+ +
+

Handbook

+ Back to Home +
+

Single documentation entry point for both operational knowledge and long-term engineering knowledge.

+ +
+
+
Operations
+

Project Wiki

+

Operational and product-level documentation for onboarding, offboarding, PDFs, integrations, admin tools, and system behavior.

+
    +
  • workflow overview
  • +
  • admin tools and system behavior
  • +
  • integrations and operations
  • +
  • runbook and troubleshooting
  • +
+ +
+ +
+
Engineering
+

Developer Handbook

+

Engineering documentation for architecture, local setup, Docker, migrations, translations, deployment, testing, and long-term maintenance.

+
    +
  • repository and service structure
  • +
  • Docker and migration workflow
  • +
  • translation and builder architecture
  • +
  • deployment, security, and maintenance notes
  • +
+ +
+
+
+ + diff --git a/backend/workflows/templates/workflows/home.html b/backend/workflows/templates/workflows/home.html index 569a8bc..44d0984 100644 --- a/backend/workflows/templates/workflows/home.html +++ b/backend/workflows/templates/workflows/home.html @@ -56,12 +56,21 @@ background: #fff; } + .brand-wrap { + display: flex; + flex-direction: column; + gap: 8px; + flex: 0 0 auto; + min-width: 0; + } + .brand-logo { width: 210px; max-width: 100%; height: auto; display: block; margin: 0; + flex: 0 0 auto; } .quick-actions { @@ -436,7 +445,9 @@
- +
+ +
{% csrf_token %} @@ -549,9 +560,9 @@ {% trans "Öffnen" %}
-

{% trans "Projekt Wiki" %}

-

{% trans "Dokumentation, Architektur und Runbook." %}

-{% trans "Öffnen" %} +

{% trans "Handbook" %}

+

{% trans "Project wiki and developer documentation in one place." %}

+{% trans "Öffnen" %}

{% trans "Integrationen" %}

diff --git a/backend/workflows/templates/workflows/integrations_setup.html b/backend/workflows/templates/workflows/integrations_setup.html index 38aef19..299c355 100644 --- a/backend/workflows/templates/workflows/integrations_setup.html +++ b/backend/workflows/templates/workflows/integrations_setup.html @@ -73,7 +73,7 @@

Integrationen Setup

diff --git a/backend/workflows/templates/workflows/intro_builder.html b/backend/workflows/templates/workflows/intro_builder.html index e8b0d29..57c2e94 100644 --- a/backend/workflows/templates/workflows/intro_builder.html +++ b/backend/workflows/templates/workflows/intro_builder.html @@ -35,7 +35,7 @@
@@ -66,9 +66,13 @@
- +
+
+ + +
@@ -85,7 +89,8 @@ Sortierung Abschnitt - Checklistenpunkt + Checklistenpunkt (DE) + Checklistenpunkt (EN) Feld-Bedingung Operator Wert @@ -108,6 +113,7 @@ +