diff --git a/airflow-core/src/airflow/ui/public/i18n/README.md b/airflow-core/src/airflow/ui/public/i18n/README.md index 18e4d44284dd9..60eb4485c5f3e 100644 --- a/airflow-core/src/airflow/ui/public/i18n/README.md +++ b/airflow-core/src/airflow/ui/public/i18n/README.md @@ -316,6 +316,18 @@ Adding missing translations (with `TODO: translate` prefix): uv run dev/i18n/check_translations_completeness.py --language --add-missing ``` +You can also remove extra translations from the language of your choice: + +```bash +uv run dev/i18n/check_translations_completeness.py --language --remove-extra +``` + +Or from all languages: + +```bash +uv run dev/i18n/check_translations_completeness.py --remove-extra +``` + The script is also added as a prek hook (manual) so that it can be run from within `prek` and CI: ```bash diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ar/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/ar/components.json index 0b1c5d74aa9c8..df1b9d69d9b09 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ar/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ar/components.json @@ -120,7 +120,6 @@ "limitedList.clickToInteract": "انقر على علامة لتصفية Dags", "limitedList.clickToOpenFull": "انقر \"+{{count}} المزيد\" لعرض القائمة الكاملة", "limitedList.copyPasteText": "يمكنك نسخ ولصق النص أعلاه", - "limitedList.showingItems": "عرض {{count}} عنصرًا", "logs": { "file": "ملف", "location": "سطر {{line}} في {{name}}" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ar/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/ar/dags.json index b7f8e60bbd8b5..12961f79af68d 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ar/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ar/dags.json @@ -20,8 +20,7 @@ "all": "الكل", "paused": "متوقف" }, - "runIdPatternFilter": "بحث تشغيلات الDag", - "triggeringUserNameFilter": "البحث حسب المستخدم صاحب الاطلاق" + "runIdPatternFilter": "بحث تشغيلات الDag" }, "ownerLink": "رابط المالك لـ{{owner}}", "runAndTaskActions": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ca/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/ca/components.json index d5e7b5c197c12..b63b043d53f68 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ca/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ca/components.json @@ -92,7 +92,6 @@ "limitedList.clickToInteract": "Fes clic en una etiqueta per filtrar els Dags", "limitedList.clickToOpenFull": "Fes clic a \"+{{count}} més\" per veure la llista completa", "limitedList.copyPasteText": "Pots copiar i enganxar el text de dalt", - "limitedList.showingItems": "Mostrant {{count}} elements", "logs": { "file": "Fitxer", "location": "línia {{line}} a {{name}}" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/de/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/de/common.json index e4d46a928f37e..fe14e8e0d6713 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/de/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/de/common.json @@ -78,11 +78,6 @@ "githubRepo": "GitHub Ablage", "restApiReference": "REST API Referenz" }, - "download": { - "download": "Herunterladen", - "hotkey": "d", - "tooltip": "Drücken Sie {{hotkey}}, um Protokolle herunterzuladen" - }, "duration": "Laufzeit", "endDate": "Enddatum", "error": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/de/hitl.json b/airflow-core/src/airflow/ui/public/i18n/locales/de/hitl.json index 48212bcd9d40b..56162417601cf 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/de/hitl.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/de/hitl.json @@ -1,8 +1,5 @@ { "filters": { - "body": "Nachricht", - "createdAtFrom": "Erstellt von ", - "createdAtTo": "Erstellt bis ", "response": { "all": "Alle", "pending": "Ausstehend", @@ -15,13 +12,11 @@ "requiredActionCount_other": "{{count}} offene Interaktionen", "requiredActionState": "Status der Interaktion", "response": { - "created": "Antwort erstellt um ", "error": "Senden der Antwort fehlgeschlagen", "optionsDescription": "Wählen Sie Ihre Optionen für diesen Task", "optionsLabel": "Optionen", "received": "Antwort empfangen um ", "respond": "Antworten", - "responded_by_user_name": "Beantwortet von (Benutzername)", "success": "{{taskId}} Interaktion erfolgreich", "title": "Erforderliche Interaktion - {{taskId}}" }, diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/el/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/el/common.json index f3b725d02fd98..2870dda2c272d 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/el/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/el/common.json @@ -101,8 +101,6 @@ }, "filter": "Φίλτρο", "filters": { - "durationFrom": "Διάρκεια Από", - "durationTo": "Διάρκεια Έως", "logicalDateFrom": "Λογική Ημερομηνία Από", "logicalDateTo": "Λογική Ημερομηνία Έως", "runAfterFrom": "Εκτέλεση Μετά Από", @@ -163,7 +161,6 @@ }, "tooltip": "Πατήστε {{hotkey}} για κύλιση προς τα {{direction}}" }, - "seconds": "{{count}}δ", "security": { "actions": "Ενέργειες", "permissions": "Δικαιώματα", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/es/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/es/components.json index 20f31033ee064..ad1edcd1b3967 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/es/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/es/components.json @@ -99,7 +99,6 @@ "limitedList.clickToInteract": "Haz clic en una etiqueta para filtrar Dags", "limitedList.clickToOpenFull": "Haz clic en \"+{{count}} más\" para ver la lista completa", "limitedList.copyPasteText": "Puedes copiar y pegar el texto de arriba", - "limitedList.showingItems": "Mostrando {{count}} elementos", "logs": { "file": "Archivo", "location": "línea {{line}} en {{name}}" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/es/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/es/dags.json index 7d93addccfc72..353ae56534a77 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/es/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/es/dags.json @@ -20,8 +20,7 @@ "all": "Todos", "paused": "Pausado" }, - "runIdPatternFilter": "Buscar Ejecuciones de Dag", - "triggeringUserNameFilter": "Buscar por usuario que activó" + "runIdPatternFilter": "Buscar Ejecuciones de Dag" }, "ownerLink": "Enlace de Propietario para {{owner}}", "runAndTaskActions": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/fr/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/fr/dags.json index cd6b2c2735631..b8a68745b81f5 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/fr/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/fr/dags.json @@ -20,8 +20,7 @@ "all": "Tous", "paused": "En pause" }, - "runIdPatternFilter": "Rechercher des exécutions de Dag", - "triggeringUserNameFilter": "Rechercher par utilisateur déclencheur" + "runIdPatternFilter": "Rechercher des exécutions de Dag" }, "ownerLink": "Lien du propriétaire pour {{owner}}", "runAndTaskActions": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/he/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/he/components.json index 424d5f675b14d..453f381980163 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/he/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/he/components.json @@ -99,7 +99,6 @@ "limitedList.clickToInteract": "לחץ על תגית כדי לסנן Dags", "limitedList.clickToOpenFull": "לחץ על \"+{{count}} נוספים\" כדי לפתוח את התצוגה המלאה", "limitedList.copyPasteText": "ניתן להעתיק ולהדביק את הטקסט שמעל", - "limitedList.showingItems": "מציג {{count}} פריטים", "logs": { "file": "קובץ", "location": "שורה {{line}} ב{{name}}" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/he/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/he/dags.json index 15729c5f1e866..1b3b06d4bf6b3 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/he/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/he/dags.json @@ -20,8 +20,7 @@ "all": "הכל", "paused": "מושהה" }, - "runIdPatternFilter": "חפש הרצת Dag", - "triggeringUserNameFilter": "חפש לפי שם המשתמש המפעיל" + "runIdPatternFilter": "חפש הרצת Dag" }, "ownerLink": "קישור בעלים ל-{{owner}}", "runAndTaskActions": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hi/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/hi/common.json index a506b9198b85c..1dcef4cb0cc1b 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hi/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hi/common.json @@ -25,7 +25,6 @@ "dag_other": "डैग्स", "dagDetails": { "catchup": "पकड़ना", - "concurrency": "समानांतरता", "dagRunTimeout": "डैग रन टाइमआउट", "defaultArgs": "डिफ़ॉल्ट तर्क", "description": "विवरण", @@ -188,8 +187,6 @@ "up_for_retry": "पुनः प्रयास के लिए", "upstream_failed": "अपस्ट्रीम विफल" }, - "switchToDarkMode": "डार्क मोड में स्विच करें", - "switchToLightMode": "लाइट मोड में स्विच करें", "table": { "completedAt": "पर पूर्ण", "createdAt": "पर बनाया", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hi/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/hi/components.json index 4efe768e9bebf..d3caf8c34c5eb 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hi/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hi/components.json @@ -92,7 +92,6 @@ "limitedList.clickToInteract": "टैग पर क्लिक करके Dags फ़िल्टर करें", "limitedList.clickToOpenFull": "\"+{{count}} और\" पर क्लिक करके पूरी सूची खोलें", "limitedList.copyPasteText": "आप ऊपर दिए गए पाठ को कॉपी और पेस्ट कर सकते हैं", - "limitedList.showingItems": "{{count}} आइटम दिखाए जा रहे हैं", "logs": { "file": "फ़ाइल", "location": "{{name}} में लाइन {{line}}" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hi/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/hi/dag.json index 253ed9585407c..36e5d3adfca97 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hi/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hi/dag.json @@ -40,7 +40,6 @@ "warning": "WARNING" }, "navigation": { - "jump": "जंप: Shift+{{arrow}}", "navigation": "नेवीगेशन: {{arrow}}", "toggleGroup": "ग्रुप टॉगल करें: Space" }, @@ -64,9 +63,7 @@ }, "panel": { "buttons": { - "options": "विकल्प", - "showGraph": "ग्राफ़ दिखाएं", - "showGrid": "ग्रिड दिखाएं" + "options": "विकल्प" }, "dagRuns": { "label": "डैग रन्स की संख्या" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hi/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/hi/dags.json index 54cac11533683..74c112d0dfbcb 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hi/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hi/dags.json @@ -20,8 +20,7 @@ "all": "सभी", "paused": "रोका गया" }, - "runIdPatternFilter": "डैग रन्स खोजें", - "triggeringUserNameFilter": "ट्रिगर करने वाले उपयोगकर्ता द्वारा खोजें" + "runIdPatternFilter": "डैग रन्स खोजें" }, "ownerLink": "{{owner}} के लिए स्वामी लिंक", "runAndTaskActions": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hu/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/hu/common.json index f26a1540f08ac..084a73f3c448e 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hu/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hu/common.json @@ -189,8 +189,6 @@ "up_for_retry": "Újrapróbálkozásra vár", "upstream_failed": "Előfeltétel sikertelen" }, - "switchToDarkMode": "Váltás sötét módra", - "switchToLightMode": "Váltás világos módra", "table": { "completedAt": "Befejezve ekkor", "createdAt": "Létrehozva ekkor", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hu/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/hu/components.json index ded34869e9dac..014e8242b43a7 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hu/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hu/components.json @@ -92,7 +92,6 @@ "limitedList.clickToInteract": "Kattintson egy címkére a Dag-ok szűréséhez", "limitedList.clickToOpenFull": "Kattintson a \"+{{count}} további\" gombra a teljes nézethez", "limitedList.copyPasteText": "A fenti szöveg másolható és beilleszthető", - "limitedList.showingItems": "{{count}} elem megjelenítése", "logs": { "file": "Fájl", "location": "{{name}} fájl {{line}}. sora" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hu/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/hu/dags.json index 93a925c7c4d7e..6394092f74718 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hu/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hu/dags.json @@ -20,8 +20,7 @@ "all": "Összes", "paused": "Szüneteltetett" }, - "runIdPatternFilter": "Dag futások keresése", - "triggeringUserNameFilter": "Keresés indító felhasználó alapján" + "runIdPatternFilter": "Dag futások keresése" }, "ownerLink": "Tulajdonosi hivatkozás: {{owner}}", "runAndTaskActions": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/it/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/it/components.json index e8903d951a2a8..3978d69905b7e 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/it/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/it/components.json @@ -106,7 +106,6 @@ "limitedList.clickToInteract": "Fai clic su un'etichetta per filtrare i Dag", "limitedList.clickToOpenFull": "Fai clic su \"+{{count}} altro\" per visualizzare tutto", "limitedList.copyPasteText": "Puoi copiare e incollare il testo sopra", - "limitedList.showingItems": "Visualizzazione di {{count}} elementi", "logs": { "file": "File", "location": "linea {{line}} in {{name}}" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/it/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/it/dags.json index 0cb4eac8ac761..6c6ab552ddc9c 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/it/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/it/dags.json @@ -20,8 +20,7 @@ "all": "Tutti", "paused": "In Pausa" }, - "runIdPatternFilter": "Cercare Dag Runs", - "triggeringUserNameFilter": "Cercare per Utente che ha Attivato" + "runIdPatternFilter": "Cercare Dag Runs" }, "ownerLink": "Link dell'Owner per {{owner}}", "runAndTaskActions": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ko/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/ko/components.json index e4dfd34a9f500..190a430bd44e9 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ko/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ko/components.json @@ -90,7 +90,6 @@ "limitedList.clickToInteract": "태그를 클릭하여 Dag를 필터링하세요", "limitedList.clickToOpenFull": "\"+{{count}}개 더 보기\"를 클릭하여 전체 보기 열기", "limitedList.copyPasteText": "위의 텍스트를 복사하여 붙여넣을 수 있습니다", - "limitedList.showingItems": "{{count}}개 항목 표시 중", "logs": { "file": "파일", "location": "{{name}}의 {{line}}번째 줄" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json index 554d8d5263e1a..fc722b1f826ae 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json @@ -95,9 +95,7 @@ "buttons": { "options": "옵션", "showGantt": "간트 차트 보기", - "showGraph": "그래프 보기", "showGraphShortcut": "그래프 보기 (g 키)", - "showGrid": "그리드 보기", "showGridShortcut": "그리드 보기 (g 키)" }, "dagRuns": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/nl/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/nl/components.json index 83785a7112a41..9997e06717296 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/nl/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/nl/components.json @@ -92,7 +92,6 @@ "limitedList.clickToInteract": "Klik op een label om Dags te filteren", "limitedList.clickToOpenFull": "Klik op \"+{{count}} meer\" om de volledige lijst te openen", "limitedList.copyPasteText": "Je kunt de bovenstaande tekst kopiëren en plakken", - "limitedList.showingItems": "{{count}} items weergegeven", "logs": { "file": "Bestand", "location": "regel {{line}} in {{name}}" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/nl/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/nl/dags.json index b57b2290a9232..23fdb3bc2d480 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/nl/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/nl/dags.json @@ -20,8 +20,7 @@ "all": "Alles", "paused": "Gepauzeerd" }, - "runIdPatternFilter": "Zoek Dag Runs", - "triggeringUserNameFilter": "Zoek op Triggering User" + "runIdPatternFilter": "Zoek Dag Runs" }, "ownerLink": "Eigenaarslink voor {{owner}}", "runAndTaskActions": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pl/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/pl/common.json index eaafe9c4b5a6b..04ca807223bc1 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/pl/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pl/common.json @@ -113,8 +113,6 @@ }, "filter": "Filtr", "filters": { - "durationFrom": "Czas trwania od", - "durationTo": "Czas trwania do", "logicalDateFrom": "Data logiczna od", "logicalDateTo": "Data logiczna do", "runAfterFrom": "Uruchom po (od)", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pl/hitl.json b/airflow-core/src/airflow/ui/public/i18n/locales/pl/hitl.json index c5023a8ad121d..428f1313ce1ed 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/pl/hitl.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pl/hitl.json @@ -1,8 +1,5 @@ { "filters": { - "body": "Treść", - "createdAtFrom": "Utworzono od", - "createdAtTo": "Utworzono do", "response": { "all": "Wszystkie", "pending": "Oczekujące", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pt/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/pt/components.json index ae83855677a6c..5b30826d8b716 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/pt/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/components.json @@ -106,7 +106,6 @@ "limitedList.clickToInteract": "Clique em uma etiqueta para filtrar os Dags", "limitedList.clickToOpenFull": "Clique em \"+{{count}} mais\" para ver a lista completa", "limitedList.copyPasteText": "Você pode copiar e colar o texto acima", - "limitedList.showingItems": "Exibindo {{count}} itens", "logs": { "file": "Arquivo", "location": "linha {{line}} em {{name}}" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pt/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/pt/dags.json index 683cf090a8b15..6ac6422c19ed1 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/pt/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/dags.json @@ -20,8 +20,7 @@ "all": "Todos", "paused": "Pausado" }, - "runIdPatternFilter": "Pesquisar Execuções de DAG", - "triggeringUserNameFilter": "Pesquisar por Usuário que Disparou" + "runIdPatternFilter": "Pesquisar Execuções de DAG" }, "ownerLink": "Link do Proprietário para {{owner}}", "runAndTaskActions": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/th/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/th/common.json index 543603805bdb4..f55f65988278d 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/th/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/th/common.json @@ -101,8 +101,6 @@ }, "filter": "ตัวกรอง", "filters": { - "durationFrom": "ระยะเวลาตั้งแต่", - "durationTo": "ระยะเวลาถึง", "logicalDateFrom": "วันที่ตามกำหนดทำงานตั้งแต่", "logicalDateTo": "วันที่ตามกำหนดทำงานถึง", "runAfterFrom": "ทำงานตั้งแต่", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/tr/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/tr/components.json index 69d80a7672ba9..0a32afc28b301 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/tr/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/tr/components.json @@ -92,7 +92,6 @@ "limitedList.clickToInteract": "Bir etikete tıklayarak Dag'leri filtreleyin", "limitedList.clickToOpenFull": "\"+{{count}} daha\" tıklayarak tam görünümü açın", "limitedList.copyPasteText": "Yukarıdaki metni kopyalayıp yapıştırabilirsiniz", - "limitedList.showingItems": "{{count}} öğe gösteriliyor", "logs": { "file": "Dosya", "location": "{{name}} içinde satır {{line}}" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/tr/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/tr/dags.json index 2974a4d4f4860..8c6d021305232 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/tr/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/tr/dags.json @@ -20,8 +20,7 @@ "all": "Tümü", "paused": "Duraklatılmış" }, - "runIdPatternFilter": "Dag Çalıştırmalarında Ara", - "triggeringUserNameFilter": "Tetikleyen Kullanıcıya Göre Ara" + "runIdPatternFilter": "Dag Çalıştırmalarında Ara" }, "ownerLink": "{{owner}} için sahip bağlantısı", "runAndTaskActions": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/components.json index e7255110e8f3b..652011da2bea8 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/components.json @@ -90,7 +90,6 @@ "limitedList.clickToInteract": "点击标签以筛选 DAGs", "limitedList.clickToOpenFull": "点击 \"+{{count}} 更多\" 打开完整视图", "limitedList.copyPasteText": "你可以复制并粘贴上方文本", - "limitedList.showingItems": "显示 {{count}} 项", "logs": { "file": "文件", "location": "第 {{line}} 行,位于 {{name}}" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dags.json index b8e4f0929d4e0..90ba4ff9588cd 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dags.json @@ -20,8 +20,7 @@ "all": "全部", "paused": "暂停" }, - "runIdPatternFilter": "搜索 Dag 执行", - "triggeringUserNameFilter": "搜索触发用户名称" + "runIdPatternFilter": "搜索 Dag 执行" }, "ownerLink": "拥有者 {{owner}} 的地址", "runAndTaskActions": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/components.json index c655e241acda4..bd4fe614be232 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/components.json @@ -90,7 +90,6 @@ "limitedList.clickToInteract": "點擊標籤以篩選 Dags", "limitedList.clickToOpenFull": "點擊 \"+{{count}} 更多\" 以開啟完整檢視", "limitedList.copyPasteText": "你可以複製並貼上上方文字", - "limitedList.showingItems": "顯示 {{count}} 個項目", "logs": { "file": "檔案", "location": "第 {{line}} 行,位於 {{name}}" diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx index b43895ced2c91..6a766fb1f0ddd 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx @@ -208,11 +208,13 @@ export const PanelButtons = ({ ); return ( - - + + { setDagView("grid"); @@ -221,12 +223,13 @@ export const PanelButtons = ({ } }} title={translate("dag:panel.buttons.showGridShortcut")} - variant={dagView === "grid" ? "solid" : "outline"} > { setDagView("graph"); @@ -235,17 +238,16 @@ export const PanelButtons = ({ } }} title={translate("dag:panel.buttons.showGraphShortcut")} - variant={dagView === "graph" ? "solid" : "outline"} > - + {/* eslint-disable-next-line jsx-a11y/no-autofocus */} - diff --git a/dev/i18n/check_translations_completeness.py b/dev/i18n/check_translations_completeness.py index e3c2e0d841863..5972087b9ff7c 100755 --- a/dev/i18n/check_translations_completeness.py +++ b/dev/i18n/check_translations_completeness.py @@ -458,7 +458,13 @@ def print_translation_progress(console, locale_files, missing_counts, summary): default=False, help="Add missing translations for all languages except English, prefixed with 'TODO: translate:'.", ) -def cli(language: str | None = None, add_missing: bool = False): +@click.option( + "--remove-extra", + is_flag=True, + default=False, + help="Remove extra translations that are present in the language but missing in English.", +) +def cli(language: str | None = None, add_missing: bool = False, remove_extra: bool = False): locale_files = get_locale_files() console = Console(force_terminal=True, color_system="auto") print_locale_file_table(locale_files, console, language) @@ -481,6 +487,22 @@ def cli(language: str | None = None, add_missing: bool = False): add_missing_translations(lf.locale, filtered_summary, console) # After adding, re-run the summary for all languages summary, missing_counts = compare_keys(get_locale_files(), console) + if remove_extra and language != "en": + # Loop through all languages except 'en' and remove extra translations + if language: + language_files = [lf for lf in locale_files if lf.locale == language] + else: + language_files = [lf for lf in locale_files if lf.locale != "en"] + for lf in language_files: + filtered_summary = {} + for filename, diff in summary.items(): + filtered_summary[filename] = LocaleSummary( + missing_keys={lf.locale: diff.missing_keys.get(lf.locale, [])}, + extra_keys={lf.locale: diff.extra_keys.get(lf.locale, [])}, + ) + remove_extra_translations(lf.locale, filtered_summary, console) + # After removing, re-run the summary for all languages + summary, missing_counts = compare_keys(get_locale_files(), console) if language: locales = [lf.locale for lf in locale_files] if language not in locales: @@ -594,5 +616,51 @@ def natural_key_sort(obj): console.print(f"[green]Added missing translations to {lang_path}[/green]") +def remove_extra_translations(language: str, summary: dict[str, LocaleSummary], console: Console): + """ + Remove extra translations for the selected language. + + Removes keys that are present in the language file but missing in the English file. + """ + for filename, diff in summary.items(): + extra_keys = set(diff.extra_keys.get(language, [])) + if not extra_keys: + continue + lang_path = LOCALES_DIR / language / filename + try: + lang_data = load_json(lang_path) + except Exception as e: + console.print(f"[yellow]Failed to load {language} file {lang_path}: {e}[/yellow]") + continue + + # Helper to recursively remove extra keys + def remove_keys(dst, prefix=""): + keys_to_remove = [] + for k, v in list(dst.items()): + full_key = f"{prefix}.{k}" if prefix else k + if full_key in extra_keys: + keys_to_remove.append(k) + elif isinstance(v, dict): + remove_keys(v, full_key) + # Remove empty dictionaries after recursion + if not v: + keys_to_remove.append(k) + for k in keys_to_remove: + del dst[k] + + remove_keys(lang_data) + + def natural_key_sort(obj): + if isinstance(obj, dict): + return {k: natural_key_sort(obj[k]) for k in sorted(obj)} + return obj + + lang_data = natural_key_sort(lang_data) + with open(lang_path, "w", encoding="utf-8") as f: + json.dump(lang_data, f, ensure_ascii=False, indent=2) + f.write("\n") # Ensure newline at the end of the file + console.print(f"[green]Removed {len(extra_keys)} extra translations from {lang_path}[/green]") + + if __name__ == "__main__": cli() diff --git a/scripts/ci/prek/check_template_context_variable_in_sync.py b/scripts/ci/prek/check_template_context_variable_in_sync.py index 0b74e4beedbd8..1c55fbd19208e 100755 --- a/scripts/ci/prek/check_template_context_variable_in_sync.py +++ b/scripts/ci/prek/check_template_context_variable_in_sync.py @@ -83,17 +83,25 @@ def extract_keys_from_dict(node: ast.Dict) -> typing.Iterator[str]: yield key.value # Extract keys from the main `context` dictionary assignment - context_assignment = next( + context_assignment: ast.AnnAssign = next( stmt for stmt in fn_get_template_context.body if isinstance(stmt, ast.AnnAssign) - and isinstance(stmt.target, ast.Name) - and stmt.target.id == "context" + and isinstance(stmt.target, ast.Attribute) + and isinstance(stmt.target.value, ast.Name) + and stmt.target.value.id == "self" + and stmt.target.attr == "_context" ) - if not isinstance(context_assignment.value, ast.Dict): + if not isinstance(context_assignment.value, ast.BoolOp): + raise TypeError("Expected a BoolOp like 'self._context or {...}'.") + + context_assignment_op = context_assignment.value + _, context_assignment_value = context_assignment_op.values + + if not isinstance(context_assignment_value, ast.Dict): raise ValueError("'context' is not assigned a dictionary literal") - yield from extract_keys_from_dict(context_assignment.value) + yield from extract_keys_from_dict(context_assignment_value) # Handle keys added conditionally in `if from_server` for stmt in fn_get_template_context.body: diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py b/task-sdk/src/airflow/sdk/execution_time/task_runner.py index 409982d1a6bf9..7138598027126 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -131,6 +131,9 @@ class RuntimeTaskInstance(TaskInstance): task: BaseOperator bundle_instance: BaseDagBundle + _context: Context | None = None + """The Task Instance context.""" + _ti_context_from_server: Annotated[TIRunContext | None, Field(repr=False)] = None """The Task Instance context from the API server, if any.""" @@ -173,7 +176,9 @@ def get_template_context(self) -> Context: validated_params = process_params(self.task.dag, self.task, dag_run_conf, suppress_exception=False) - context: Context = { + # Cache the context object, which ensures that all calls to get_template_context + # are operating on the same context object. + self._context: Context = self._context or { # From the Task Execution interface "dag": self.task.dag, "inlets": self.task.inlets, @@ -213,7 +218,7 @@ def get_template_context(self) -> Context: lambda: coerce_datetime(get_previous_dagrun_success(self.id).end_date) ), } - context.update(context_from_server) + self._context.update(context_from_server) if logical_date := coerce_datetime(dag_run.logical_date): if TYPE_CHECKING: @@ -224,7 +229,7 @@ def get_template_context(self) -> Context: ts_nodash = logical_date.strftime("%Y%m%dT%H%M%S") ts_nodash_with_tz = ts.replace("-", "").replace(":", "") # logical_date and data_interval either coexist or be None together - context.update( + self._context.update( { # keys that depend on logical_date "logical_date": logical_date, @@ -251,7 +256,7 @@ def get_template_context(self) -> Context: # existence. Should this be a private attribute on RuntimeTI instead perhaps? setattr(self, "_upstream_map_indexes", from_server.upstream_map_indexes) - return context + return self._context def render_templates( self, context: Context | None = None, jinja_env: jinja2.Environment | None = None diff --git a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py index 930d80590ea7b..a2a19266ae5aa 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py +++ b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py @@ -65,7 +65,7 @@ ) from airflow.sdk.bases.xcom import BaseXCom from airflow.sdk.definitions._internal.types import NOTSET, SET_DURING_EXECUTION, ArgNotSet -from airflow.sdk.definitions.asset import Asset, AssetAlias, Dataset, Model +from airflow.sdk.definitions.asset import Asset, AssetAlias, AssetUniqueKey, Dataset, Model from airflow.sdk.definitions.param import DagParam from airflow.sdk.exceptions import ErrorType from airflow.sdk.execution_time.comms import ( @@ -2482,6 +2482,32 @@ def on_task_instance_failed(self, previous_state, task_instance, error): def before_stopping(self, component): self.component = component + class CustomOutletEventsListener: + def __init__(self): + self.outlet_events = [] + self.error = None + + def _add_outlet_events(self, context): + outlets = context["outlets"] + for outlet in outlets: + self.outlet_events.append(context["outlet_events"][outlet]) + + @hookimpl + def on_task_instance_running(self, previous_state, task_instance): + context = task_instance.get_template_context() + self._add_outlet_events(context) + + @hookimpl + def on_task_instance_success(self, previous_state, task_instance): + context = task_instance.get_template_context() + self._add_outlet_events(context) + + @hookimpl + def on_task_instance_failed(self, previous_state, task_instance, error): + context = task_instance.get_template_context() + self._add_outlet_events(context) + self.error = error + @pytest.fixture(autouse=True) def clean_listener_manager(self): lm = get_listener_manager() @@ -2601,6 +2627,118 @@ def execute(self, context): assert listener.state == [TaskInstanceState.RUNNING, TaskInstanceState.FAILED] assert listener.error == error + def test_listener_access_outlet_event_on_running_and_success(self, mocked_parse, mock_supervisor_comms): + """Test listener can access outlet events through invoking get_template_context() while task running and success""" + listener = self.CustomOutletEventsListener() + get_listener_manager().add_listener(listener) + + test_asset = Asset("test-asset") + test_key = AssetUniqueKey(name="test-asset", uri="test-asset") + test_extra = {"name1": "value1", "nested_obj": {"name2": "value2"}} + + class Producer(BaseOperator): + def execute(self, context): + outlet_events = context["outlet_events"] + outlet_events[test_asset].extra = test_extra + + task = Producer( + task_id="test_listener_access_outlet_event_on_running_and_success", outlets=[test_asset] + ) + dag = get_inline_dag(dag_id="test_dag", task=task) + ti = TaskInstance( + id=uuid7(), + task_id=task.task_id, + dag_id=dag.dag_id, + run_id="test_run", + try_number=1, + dag_version_id=uuid7(), + ) + + runtime_ti = RuntimeTaskInstance.model_construct( + **ti.model_dump(exclude_unset=True), task=task, start_date=timezone.utcnow() + ) + + log = mock.MagicMock() + context = runtime_ti.get_template_context() + + with mock.patch( + "airflow.sdk.execution_time.task_runner._validate_task_inlets_and_outlets" + ) as validate_mock: + state, _, _ = run(runtime_ti, context, log) + + validate_mock.assert_called_once() + + outlet_event_accessor = listener.outlet_events.pop() + assert outlet_event_accessor.key == test_key + assert outlet_event_accessor.extra == test_extra + + finalize(runtime_ti, state, context, log) + + outlet_event_accessor = listener.outlet_events.pop() + assert outlet_event_accessor.key == test_key + assert outlet_event_accessor.extra == test_extra + + @pytest.mark.parametrize( + "exception", + [ + ValueError("oops"), + SystemExit("oops"), + AirflowException("oops"), + ], + ids=["ValueError", "SystemExit", "AirflowException"], + ) + def test_listener_access_outlet_event_on_failed(self, mocked_parse, mock_supervisor_comms, exception): + """Test listener can access outlet events through invoking get_template_context() while task failed""" + listener = self.CustomOutletEventsListener() + get_listener_manager().add_listener(listener) + + test_asset = Asset("test-asset") + test_key = AssetUniqueKey(name="test-asset", uri="test-asset") + test_extra = {"name1": "value1", "nested_obj": {"name2": "value2"}} + + class Producer(BaseOperator): + def execute(self, context): + outlet_events = context["outlet_events"] + outlet_events[test_asset].extra = test_extra + raise exception + + task = Producer(task_id="test_listener_access_outlet_event_on_failed", outlets=[test_asset]) + dag = get_inline_dag(dag_id="test_dag", task=task) + ti = TaskInstance( + id=uuid7(), + task_id=task.task_id, + dag_id=dag.dag_id, + run_id="test_run", + try_number=1, + dag_version_id=uuid7(), + ) + + runtime_ti = RuntimeTaskInstance.model_construct( + **ti.model_dump(exclude_unset=True), task=task, start_date=timezone.utcnow() + ) + + log = mock.MagicMock() + context = runtime_ti.get_template_context() + + with mock.patch( + "airflow.sdk.execution_time.task_runner._validate_task_inlets_and_outlets" + ) as validate_mock: + state, _, error = run(runtime_ti, context, log) + + validate_mock.assert_called_once() + + outlet_event_accessor = listener.outlet_events.pop() + assert outlet_event_accessor.key == test_key + assert outlet_event_accessor.extra == test_extra + + finalize(runtime_ti, state, context, log, error) + + outlet_event_accessor = listener.outlet_events.pop() + assert outlet_event_accessor.key == test_key + assert outlet_event_accessor.extra == test_extra + + assert listener.error == error + @pytest.mark.usefixtures("mock_supervisor_comms") class TestTaskRunnerCallsCallbacks: