mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-24 03:55:34 +08:00
* feat: add IBM Globalization Pipeline integration and i18n setup
- Add GP REST API client with GP-HMAC authentication (scripts/gp/gp_client.py)
- Add upload/download scripts for syncing strings with GP
- Set up i18next + react-i18next with 7 language support (en, fr, ja, es, de, pt, zh-Hans)
- Add 48 UI strings in flat dot notation to en.json (from alerts_constants)
- Add GitHub Actions for automated upload on en.json changes and daily translation download
* [autofix.ci] apply automated fixes
* fix: resolve ruff linting issues in GP scripts
- Fix quote style, import ordering, docstring format
- Use Path instead of os.makedirs/open/os.path.join
- Add missing timeouts to requests calls
- Move noqa comments to correct lines for S501
* feat: migrate all frontend UI strings to i18n for GP localization
- Replace all hardcoded user-facing strings with t() calls across 80+ components, pages, modals, and utilities
- Add 326 translation keys to en.json covering errors, alerts, dialogs, chat, flow, settings, store, table, output, nav, auth, and more
- Add translated locale files for fr, es, de, pt, ja, zh-Hans (downloaded from Globalization Pipeline)
- Migrate alerts_constants.tsx and constants.ts string usages to react-i18next
- Support non-React files (stores, utils) via i18n.t() direct calls
* feat: migrate sidebar strings to i18n (Phase 4)
- Add 56 new translation keys for sidebar categories, nav items, labels, buttons, and empty states
- Convert SIDEBAR_CATEGORIES display_names to i18n key references in styleUtils.ts
- Update 13 sidebar components to use t() for all user-facing strings
- Refresh all 6 locale files from GP (382 total keys)
- Add BACKEND_STRINGS_STRATEGY.md documenting plan for component display_name i18n
* [autofix.ci] apply automated fixes
* fix: add react-i18next mock and fix sidebar nav tests for i18n migration
- Add global react-i18next mock in jest.setup.js using en.json lookups so
t(key) returns English strings instead of raw keys in test environment
- Fix sidebarSegmentedNav tests: update NAV_ITEMS expectations to use i18n
keys, and use en.json lookups for tooltip/accessibility label assertions
* feat: add LanguageSelector dropdown to globalization pipeline
Cherry-picked frontend-only changes from feat/gp-backend-i18n (6ab21559db, 3e274a84e3):
- Add LanguageSelector component with dropdown to switch UI language
- Wire LanguageSelector into AccountMenu header
- Persist language preference to localStorage in i18n.ts
- Send Accept-Language header via useCustomApiHeaders (reactive via useTranslation)
No backend files included.
* [autofix.ci] apply automated fixes
* ci: add GP_TEST environment to gp-download and gp-upload workflows
* temp: add feature branch push trigger for workflow testing
* temp: trigger gp-upload workflow test
* fix: use correct GP_test environment name in workflows
* fix: use vars for GP_INSTANCE and GP_BUNDLE
* [autofix.ci] apply automated fixes
* fix: correct environment name to GP-test (hyphen not underscore)
* chore: update translations from Globalization Pipeline [skip ci]
* [autofix.ci] apply automated fixes
* chore: update translations from Globalization Pipeline [skip ci]
* temp: trigger gp-upload workflow test
* ci: add download-gp-bundle job to nightly build pipeline
Downloads frontend translations from Globalization Pipeline after
create-nightly-tag and before frontend tests run, ensuring tests
always validate the latest translated locale files.
- Frontend tests (Linux/Windows) now depend on download-gp-bundle
- Backend unit tests are unaffected (no frontend locale dependency)
- GP failure blocks release-nightly-build (same severity as linux tests)
* chore: remove temporary dev files from gp scripts directory
Remove BACKEND_STRINGS_STRATEGY.md (internal planning doc), test_auth.py
(GP auth debugging script), and en.json (dev-time string snapshot).
Also add venv/ to .gitignore to prevent accidental commits of the local virtualenv.
* [autofix.ci] apply automated fixes
* chore: update translations from Globalization Pipeline [skip ci]
* [autofix.ci] apply automated fixes
* chore: update translations from Globalization Pipeline [skip ci]
* feat(i18n): move language selector from account menu to Settings > General
- Add LanguageForm card component in GeneralPage following the existing
Card pattern (ProfilePictureForm), with immediate language switching
- English marked as "(Recommended)" via i18n key
- Remove Language row from AccountMenu dropdown
- Add settings.languageTitle/Description/Recommended keys to en.json
for upload to Globalization Pipeline
* feat(i18n): refactor language constants and improve GP pipeline
- Extract SUPPORTED_LANGUAGES into src/frontend/src/constants/languages.ts
- Update LanguageForm to import from shared constants, add aria-label
- Add settings.languageSelectAriaLabel key to en.json
- Update LanguageSelector component to use shared SUPPORTED_LANGUAGES
- Fix gp_client and download_translations scripts
- Update GP upload/download/nightly GitHub Actions workflows
- Add GP script tests
* [autofix.ci] apply automated fixes
* ci(gp): trigger upload on release branches, download via dispatch only
- Upload now triggers on push to release-* branches (instead of main)
so translators get strings once they're finalized on the release branch
- Download removes the daily cron schedule; workflow_dispatch only
since the nightly build already handles scheduled runs
* chore: update translations from Globalization Pipeline
* fix(i18n): address PR review comments — tests, nightly build decoupling, GP test workflow
- Add pytest tests for gp_client.py (HMAC auth, HTTP errors, timeout)
- Add pytest tests for upload_strings.py and download_translations.py
- Add conftest.py to set sys.path so GP scripts can be imported from repo root
- Add scripts/gp/__init__.py to make gp a proper package for test imports
- Add gp-test.yml workflow to run GP script tests on PRs touching scripts/gp/**
- Decouple download-gp-bundle from nightly build chain:
- Add continue-on-error: true so GP failure never blocks the build
- Remove download-gp-bundle from needs/if of frontend-tests-linux,
frontend-tests-windows, and release-nightly-build
- Add warning annotation step that fires on GP download failure
* feat(i18n): lazy-load non-English locale files via Vite dynamic imports
- Remove 6 static locale imports from i18n.ts; only en.json is bundled
- Export loadLanguage() which dynamically imports a locale chunk on demand
and caches it via i18n.hasResourceBundle (no-op on repeat calls)
- Update index.tsx to await loadLanguage(detectedLang) before rendering,
preventing an English flash for non-English users
- Make handleChange async in LanguageForm and LanguageSelector to await
loadLanguage before calling i18n.changeLanguage
- Add i18n.test.ts covering loadLanguage: en no-op, new language load,
cache hit, and multiple independent languages
- Update LanguageForm.test.tsx to mock @/i18n and assert loadLanguage
is called before changeLanguage on language switch
* [autofix.ci] apply automated fixes
* [autofix.ci] apply automated fixes (attempt 2/3)
* fix: add ruff ignores for scripts/gp/tests
- Add per-file ignores for scripts/gp/tests/* to fix CI failures
- Ignore S101 (assert usage), TRY003, EM101 for test files
- Follows existing pattern for other test directories
* [autofix.ci] apply automated fixes
* fix(ci): run GP translation download before nightly tag creation
Previously download-gp-bundle ran after create-nightly-tag, so fresh
translations were committed to the branch after the tag was already
created. The nightly Docker image builds from the tag, meaning it never
included the day's translations.
Fix: move download-gp-bundle to run before create-nightly-tag so the
tag is created on a commit that already includes the latest translations.
* fix(ui): replace native select with Radix UI Select in LanguageForm
Swaps the native <select> element for the existing Radix UI Select
component to get consistent padding on both sides of the chevron,
removing reliance on browser-rendered chevron positioning.
* fix(ci): rearchitect GP translation download to use PRs instead of direct push
Replace direct branch commits in gp-download.yml and nightly_build.yml with
a PR-based flow using peter-evans/create-pull-request, respecting release
branch protection policies.
- gp-download.yml: dynamically resolves latest release-* branch, closes stale
translation PRs, opens a new PR via peter-evans/create-pull-request@v8, and
enables auto-merge (squash); scheduled at 23:00 UTC (1h before nightly)
- nightly_build.yml: remove download-gp-bundle job and its dependency from
create-nightly-tag — nightly build now uses translations already in the branch
* fix(ci): skip GP upload when triggered from an outdated release branch
Add a branch guard step that resolves the latest release-* branch at
runtime and skips the upload (with a notice annotation) if the triggering
branch is not the latest. Prevents stale en.json from an old release branch
overwriting current source strings in the Globalization Pipeline.
* fix(tests): fix LanguageForm test suite failures
- Add `...jest.requireActual` to @tanstack/react-query mock so QueryClient
constructor remains available (used in contexts/index.tsx at module load)
- Mock @/components/ui/select with a native <select> shim so test assertions
using selectOptions() and querySelectorAll("option") work correctly
* fix: add trailing newline to component_index.json
The build_component_index.py script was generating component_index.json
without a trailing newline, causing CI failures when autofix.ci tried to
add the missing newline. This fix ensures the generated JSON file always
ends with a newline character, following POSIX text file standards.
Fixes the 'No newline at end of file' error in the update-component-index
workflow job.
* [autofix.ci] apply automated fixes
* fix: add trailing newline to component_index.json
The build_component_index.py script was generating component_index.json
without a trailing newline, causing CI failures when autofix.ci tried to
add the missing newline. This fix ensures the generated JSON file always
ends with a newline character, following POSIX text file standards.
Fixes the 'No newline at end of file' error in the update-component-index
workflow job.
* [autofix.ci] apply automated fixes
* [autofix.ci] apply automated fixes (attempt 2/3)
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: vijay kumar katuri <vijay.katuri@ibm.com>
181 lines
6.7 KiB
Python
181 lines
6.7 KiB
Python
"""Tests for gp_client.py — HMAC auth and API operations."""
|
|
|
|
import base64
|
|
import hashlib
|
|
import hmac
|
|
import importlib
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import gp_client
|
|
import pytest
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _expected_signature(password: str, message: str) -> str:
|
|
sig = hmac.new(
|
|
bytes(password, "ISO-8859-1"),
|
|
msg=bytes(message, "ISO-8859-1"),
|
|
digestmod=hashlib.sha1,
|
|
).digest()
|
|
return base64.b64encode(sig).decode()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_headers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetHeaders:
|
|
def test_get_request_authorization_format(self):
|
|
with patch.dict("os.environ", {"GP_ADMIN_USER_ID": "user123", "GP_ADMIN_PASSWORD": "secret"}):
|
|
importlib.reload(gp_client)
|
|
headers = gp_client.get_headers("https://example.com/api", "GET")
|
|
|
|
assert headers["Authorization"].startswith("GP-HMAC user123:")
|
|
assert "GP-Date" in headers
|
|
assert headers["accept"] == "application/json"
|
|
assert "Content-Type" not in headers
|
|
|
|
def test_put_request_includes_content_type(self):
|
|
with patch.dict("os.environ", {"GP_ADMIN_USER_ID": "user123", "GP_ADMIN_PASSWORD": "secret"}):
|
|
importlib.reload(gp_client)
|
|
headers = gp_client.get_headers("https://example.com/api", "PUT", {"key": "val"})
|
|
|
|
assert headers["Content-Type"] == "application/json"
|
|
|
|
def test_patch_request_uses_merge_patch_content_type(self):
|
|
with patch.dict("os.environ", {"GP_ADMIN_USER_ID": "user123", "GP_ADMIN_PASSWORD": "secret"}):
|
|
importlib.reload(gp_client)
|
|
headers = gp_client.get_headers("https://example.com/api", "PATCH", {})
|
|
|
|
assert headers["Content-Type"] == "application/merge-patch+json"
|
|
|
|
def test_hmac_signature_is_deterministic_for_same_inputs(self):
|
|
with patch.dict("os.environ", {"GP_ADMIN_USER_ID": "user123", "GP_ADMIN_PASSWORD": "secret"}):
|
|
importlib.reload(gp_client)
|
|
with patch("gp_client.datetime") as mock_dt:
|
|
mock_dt.now.return_value.strftime.return_value = "Thu, 26 Mar 2026 12:00:00 UTC"
|
|
h1 = gp_client.get_headers("https://example.com", "GET")
|
|
h2 = gp_client.get_headers("https://example.com", "GET")
|
|
|
|
assert h1["Authorization"] == h2["Authorization"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# list_bundles
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestListBundles:
|
|
def test_returns_parsed_json_on_success(self):
|
|
mock_response = MagicMock()
|
|
mock_response.json.return_value = {"bundleIds": ["langflow-ui"]}
|
|
|
|
with (
|
|
patch.dict("os.environ", {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}),
|
|
patch("gp_client.requests.get", return_value=mock_response) as mock_get,
|
|
):
|
|
importlib.reload(gp_client)
|
|
result = gp_client.list_bundles()
|
|
|
|
mock_get.assert_called_once()
|
|
mock_response.raise_for_status.assert_called_once()
|
|
assert result == {"bundleIds": ["langflow-ui"]}
|
|
|
|
def test_raises_on_http_error(self):
|
|
import requests as req
|
|
|
|
mock_response = MagicMock()
|
|
mock_response.raise_for_status.side_effect = req.exceptions.HTTPError("401 Unauthorized")
|
|
|
|
with (
|
|
patch.dict("os.environ", {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}),
|
|
patch("gp_client.requests.get", return_value=mock_response),
|
|
):
|
|
importlib.reload(gp_client)
|
|
with pytest.raises(req.exceptions.HTTPError):
|
|
gp_client.list_bundles()
|
|
|
|
def test_raises_on_timeout(self):
|
|
import requests as req
|
|
|
|
with (
|
|
patch.dict("os.environ", {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}),
|
|
patch("gp_client.requests.get", side_effect=req.exceptions.Timeout),
|
|
):
|
|
importlib.reload(gp_client)
|
|
with pytest.raises(req.exceptions.Timeout):
|
|
gp_client.list_bundles()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# upload_strings
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestUploadStrings:
|
|
def test_successful_upload(self):
|
|
mock_response = MagicMock()
|
|
mock_response.json.return_value = {"status": "ok"}
|
|
|
|
with (
|
|
patch.dict("os.environ", {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}),
|
|
patch("gp_client.requests.put", return_value=mock_response) as mock_put,
|
|
):
|
|
importlib.reload(gp_client)
|
|
result = gp_client.upload_strings({"hello": "Hello"})
|
|
|
|
mock_put.assert_called_once()
|
|
assert result == {"status": "ok"}
|
|
|
|
def test_raises_on_server_error(self):
|
|
import requests as req
|
|
|
|
mock_response = MagicMock()
|
|
mock_response.raise_for_status.side_effect = req.exceptions.HTTPError("500")
|
|
|
|
with (
|
|
patch.dict("os.environ", {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}),
|
|
patch("gp_client.requests.put", return_value=mock_response),
|
|
):
|
|
importlib.reload(gp_client)
|
|
with pytest.raises(req.exceptions.HTTPError):
|
|
gp_client.upload_strings({"hello": "Hello"})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_strings
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetStrings:
|
|
def test_returns_parsed_json(self):
|
|
mock_response = MagicMock()
|
|
mock_response.json.return_value = {"resourceStrings": {"hello": {"value": "Bonjour"}}}
|
|
|
|
with (
|
|
patch.dict("os.environ", {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}),
|
|
patch("gp_client.requests.get", return_value=mock_response),
|
|
):
|
|
importlib.reload(gp_client)
|
|
result = gp_client.get_strings("fr")
|
|
|
|
assert result["resourceStrings"]["hello"]["value"] == "Bonjour"
|
|
|
|
def test_raises_on_403(self):
|
|
import requests as req
|
|
|
|
mock_response = MagicMock()
|
|
mock_response.raise_for_status.side_effect = req.exceptions.HTTPError("403 Forbidden")
|
|
|
|
with (
|
|
patch.dict("os.environ", {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}),
|
|
patch("gp_client.requests.get", return_value=mock_response),
|
|
):
|
|
importlib.reload(gp_client)
|
|
with pytest.raises(req.exceptions.HTTPError):
|
|
gp_client.get_strings("fr")
|