Skip to content

feat: add French locale support for connection state detection#319

Closed
vrpctaywal wants to merge 1 commit intostickerdaniel:mainfrom
vrpctaywal:feat/french-locale-support
Closed

feat: add French locale support for connection state detection#319
vrpctaywal wants to merge 1 commit intostickerdaniel:mainfrom
vrpctaywal:feat/french-locale-support

Conversation

@vrpctaywal
Copy link
Copy Markdown

Summary

  • connect_with_person() always returns connect_unavailable for users whose LinkedIn interface is in French
  • Root cause: detect_connection_state() only matches English button labels (Connect, Follow, Pending) but French LinkedIn displays Se connecter, Suivre, En attente
  • The _ACTION_AREA_END regex only matches English section headings (About, Experience) but French displays Infos, Expérience, Activité

Changes

  • connection.py: detect_connection_state() now returns (state, is_french) tuple, checking both English and French button labels. Added STATE_BUTTON_MAP_FR and French section headings to _ACTION_AREA_END.
  • extractor.py: connect_with_person() selects the correct button map based on detected language. _open_more_menu() now matches both More/Plus aria-labels and Connect/Se connecter menu items.
  • Tests: Updated 8 existing tests for tuple return value + added 5 new French locale tests (all 14 pass).

Test plan

  • All 14 TestDetectConnectionState tests pass
  • Manual test: connect_with_person on a French LinkedIn profile with Se connecter button
  • Manual test: connect_with_person on a French LinkedIn profile with Suivre + More menu

🤖 Generated with Claude Code

The browser locale is forced to en-US, but for users whose LinkedIn
interface is set to French, button labels appear in French (e.g.
"Se connecter" instead of "Connect", "Suivre" instead of "Follow").

This causes connect_with_person() to always return "connect_unavailable"
for French-speaking users.

Changes:
- connection.py: detect_connection_state() now returns (state, is_french)
  tuple, checking both English and French button labels
- connection.py: Added French section headings to _ACTION_AREA_END regex
- connection.py: Added STATE_BUTTON_MAP_FR for French button text
- extractor.py: connect_with_person() uses French button map when detected
- extractor.py: _open_more_menu() matches both "More" and "Plus" aria-labels
  and both "Connect" and "Se connecter" menu items
- tests: Updated existing tests for tuple return + added 5 French locale tests

Fixes connection requests for all fr-FR LinkedIn users.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 3, 2026

Greptile Summary

This PR adds French locale (fr-FR) support to connect_with_person() so that LinkedIn users whose interface is in French no longer get a spurious connect_unavailable result. The root cause — detect_connection_state() matching only English button labels and section headings — is fixed by returning a (state, is_french) tuple and routing the button click through a new STATE_BUTTON_MAP_FR dictionary. The approach fits naturally into the existing scraping architecture.

Key changes:

  • connection.py: detect_connection_state() now returns (ConnectionState, bool). English labels are checked first; French labels are tried only if none match. French section headings (Infos, Activité, etc.) added to _ACTION_AREA_END regex.
  • extractor.py: connect_with_person() selects STATE_BUTTON_MAP_FR when is_french=True. _open_more_menu() now also matches aria-label*='Plus' and the Se connecter menu item.
  • tests/test_scraping.py: 8 existing tests updated for the tuple return value; 5 new French locale tests added.

Notable gaps:

  • No test for the French incoming_request state (Accepter / Ignorer) in TestDetectConnectionState.
  • TestConnectWithPerson has no French end-to-end test verifying click_button_by_text is called with \"Se connecter\" — and the two manual test plan items are still unchecked.
  • already_connected always returns is_french=False even when the French · 1er indicator is matched, which is semantically inconsistent (though not functionally broken today).
  • The aria-label*='Plus' CSS substring selector could be tightened to an exact match to reduce the risk of accidentally clicking the wrong button on pages where other elements contain "Plus".

Confidence Score: 4/5

Safe to merge for English users; French path is mostly correct but has test coverage gaps and two unchecked manual test items.

The core logic is correct — English detection is unchanged and French detection only activates as a fallback, so existing behaviour for English users is unaffected. The tuple return is handled consistently throughout the call chain. The main concerns are test gaps (missing French incoming_request unit test, no French path in TestConnectWithPerson) and the two manual test plan items that remain unchecked, leaving the French Suivre + More menu path unverified in production. No critical logic bugs were found.

tests/test_scraping.py — missing French incoming_request test and no French coverage in TestConnectWithPerson.

Important Files Changed

Filename Overview
linkedin_mcp_server/scraping/connection.py Adds French locale detection by returning a (state, is_french) tuple; logic is sound but already_connected always returns is_french=False even for French profiles (· 1er).
linkedin_mcp_server/scraping/extractor.py Correctly consumes the new tuple return and selects the right button map; the aria-label*='Plus' CSS substring selector is slightly broad but low-risk in practice.
tests/test_scraping.py Existing tests correctly updated for the new tuple return; missing a French incoming_request unit test and any French path coverage in TestConnectWithPerson.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[connect_with_person] --> B[scrape_person → page_text]
    B --> C[detect_connection_state]
    C --> D{· 1st / · 1er in top 300 chars?}
    D -- yes --> E[already_connected, False]
    D -- no --> F[_extract_action_area]
    F --> G{English labels?}
    G -- Pending --> H[pending, False]
    G -- Accept+Ignore --> I[incoming_request, False]
    G -- Connect --> J[connectable, False]
    G -- Follow --> K[follow_only, False]
    G -- none --> L{French labels?}
    L -- En attente --> M[pending, True]
    L -- Accepter+Ignorer --> N[incoming_request, True]
    L -- Se connecter --> O[connectable, True]
    L -- Suivre --> P[follow_only, True]
    L -- none --> Q[unavailable, False]
    E --> R[return early]
    H --> R
    J --> S{is_french?}
    O --> S
    I --> S
    N --> S
    K --> T[_open_more_menu More / Plus aria-label]
    P --> T
    T -- found Connect or Se connecter --> U[state = connectable, via_more_menu = True]
    T -- not found --> V[return follow_only]
    U --> S
    S -- False --> W[STATE_BUTTON_MAP Connect / Accept]
    S -- True --> X[STATE_BUTTON_MAP_FR Se connecter / Accepter]
    W --> Y[click_button_by_text]
    X --> Y
    Y --> Z[handle dialog → return result]
Loading

Comments Outside Diff (1)

  1. tests/test_scraping.py, line 804-837 (link)

    P2 No French path coverage in TestConnectWithPerson

    TestConnectWithPerson has no test exercising the French code path end-to-end. The critical assertion that click_button_by_text is called with "Se connecter" (rather than "Connect") for a French profile is never verified.

    Since the manual test plan items for French profiles are still unchecked (per the PR description), a unit test here would at minimum verify the button selection logic:

    async def test_connectable_fr_clicks_se_connecter(self, mock_page):
        extractor = LinkedInExtractor(mock_page)
        text = "Laurent\n\n· 2e\n\nEngineer\n\nSe connecter\nPlus\nInfos\n"
    
        with (
            patch.object(extractor, "scrape_person", self._mock_scrape(text)),
            patch.object(
                extractor,
                "click_button_by_text",
                new_callable=AsyncMock,
                return_value=True,
            ) as mock_click,
            patch.object(
                extractor,
                "_dialog_is_open",
                new_callable=AsyncMock,
                return_value=False,
            ),
        ):
            result = await extractor.connect_with_person("testuser")
    
        assert result["status"] == "connected"
        mock_click.assert_awaited_once_with("Se connecter", scope="main")
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: tests/test_scraping.py
    Line: 804-837
    
    Comment:
    **No French path coverage in `TestConnectWithPerson`**
    
    `TestConnectWithPerson` has no test exercising the French code path end-to-end. The critical assertion that `click_button_by_text` is called with `"Se connecter"` (rather than `"Connect"`) for a French profile is never verified.
    
    Since the manual test plan items for French profiles are still unchecked (per the PR description), a unit test here would at minimum verify the button selection logic:
    
    ```python
    async def test_connectable_fr_clicks_se_connecter(self, mock_page):
        extractor = LinkedInExtractor(mock_page)
        text = "Laurent\n\n· 2e\n\nEngineer\n\nSe connecter\nPlus\nInfos\n"
    
        with (
            patch.object(extractor, "scrape_person", self._mock_scrape(text)),
            patch.object(
                extractor,
                "click_button_by_text",
                new_callable=AsyncMock,
                return_value=True,
            ) as mock_click,
            patch.object(
                extractor,
                "_dialog_is_open",
                new_callable=AsyncMock,
                return_value=False,
            ),
        ):
            result = await extractor.connect_with_person("testuser")
    
        assert result["status"] == "connected"
        mock_click.assert_awaited_once_with("Se connecter", scope="main")
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix All With AI
This is a comment left during a code review.
Path: tests/test_scraping.py
Line: 767-789

Comment:
**Missing French `incoming_request` test**

The French detection for `incoming_request` (checking for both `Accepter` and `Ignorer`) has no test coverage. All other French states — `already_connected`, `connectable`, `follow_only`, `pending` — have dedicated tests, but this path is silently skipped.

A missing test here means a regression in this code path could go unnoticed. Consider adding:

```python
def test_incoming_request_fr(self):
    text = "Aklasur Rahman\n\n--\n\nDhaka\n\nAccepter\nIgnorer\nPlus"
    assert detect_connection_state(text) == ("incoming_request", True)
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: tests/test_scraping.py
Line: 804-837

Comment:
**No French path coverage in `TestConnectWithPerson`**

`TestConnectWithPerson` has no test exercising the French code path end-to-end. The critical assertion that `click_button_by_text` is called with `"Se connecter"` (rather than `"Connect"`) for a French profile is never verified.

Since the manual test plan items for French profiles are still unchecked (per the PR description), a unit test here would at minimum verify the button selection logic:

```python
async def test_connectable_fr_clicks_se_connecter(self, mock_page):
    extractor = LinkedInExtractor(mock_page)
    text = "Laurent\n\n· 2e\n\nEngineer\n\nSe connecter\nPlus\nInfos\n"

    with (
        patch.object(extractor, "scrape_person", self._mock_scrape(text)),
        patch.object(
            extractor,
            "click_button_by_text",
            new_callable=AsyncMock,
            return_value=True,
        ) as mock_click,
        patch.object(
            extractor,
            "_dialog_is_open",
            new_callable=AsyncMock,
            return_value=False,
        ),
    ):
        result = await extractor.connect_with_person("testuser")

    assert result["status"] == "connected"
    mock_click.assert_awaited_once_with("Se connecter", scope="main")
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: linkedin_mcp_server/scraping/connection.py
Line: 70-72

Comment:
**`is_french` always `False` for French already-connected profiles**

When `"· 1er"` is detected (French interface), the function still returns `is_french=False`. While this is not a functional bug today (the caller returns early for `already_connected` before consulting `is_french`), it is semantically inconsistent — a French profile is reported as non-French — and could silently mislead future callers that rely on the flag.

Consider returning `True` when the French indicator is matched:

```suggestion
    top = profile_text[:300]
    if "\u00b7 1st" in top:
        return "already_connected", False
    if "\u00b7 1er" in top:
        return "already_connected", True
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: linkedin_mcp_server/scraping/extractor.py
Line: 540-542

Comment:
**`aria-label*='Plus'` selector is overly broad**

The `*=` CSS attribute substring selector matches any button whose `aria-label` *contains* the string `"Plus"`. On a French LinkedIn page, other buttons could incidentally contain "Plus" (e.g., a hypothetical "Ajouter plus de compétences" button with `aria-label="Plus de compétences"`), causing `_open_more_menu` to click the wrong element.

LinkedIn's French three-dot button typically carries `aria-label="Plus d'options"` or `aria-label="Plus"`. A more precise match would reduce the risk:

```suggestion
        more_btn = self._page.locator(
            "main button[aria-label*='More'], main button[aria-label='Plus'], main button[aria-label=\"Plus d'options\"]"
        )
```

If the exact aria-label is confirmed through manual testing, tightening this selector would make the code more resilient against layout changes that add new "Plus"-containing buttons.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat: add French locale support for conn..." | Re-trigger Greptile

Comment thread tests/test_scraping.py
Comment on lines +767 to +789
# --- French locale tests ---

def test_already_connected_fr(self):
text = "Laurent Delade\n\n· 1er\n\nChannel Account Manager\n\nMessage\nPlus"
assert detect_connection_state(text) == ("already_connected", False)

def test_connectable_fr(self):
text = "Laurent Delade\n\n· 2e\n\nChannel Account Manager\n\nSe connecter\nEnregistrer\nPlus"
assert detect_connection_state(text) == ("connectable", True)

def test_follow_only_fr(self):
text = "Dragan Radulović\n\n· 3e\n\nPresident\n\nSuivre\nEnregistrer\nPlus"
assert detect_connection_state(text) == ("follow_only", True)

def test_pending_fr(self):
text = "Jane Doe\n\n· 2e\n\nEngineer\n\nEn attente\nPlus"
assert detect_connection_state(text) == ("pending", True)

def test_action_area_cuts_at_french_headings(self):
text = "Name\n\nSe connecter\nPlus\nInfos\n\nSuivre\nSe connecter"
area = _extract_action_area(text)
assert "Infos" not in area
assert "Se connecter" in area
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing French incoming_request test

The French detection for incoming_request (checking for both Accepter and Ignorer) has no test coverage. All other French states — already_connected, connectable, follow_only, pending — have dedicated tests, but this path is silently skipped.

A missing test here means a regression in this code path could go unnoticed. Consider adding:

def test_incoming_request_fr(self):
    text = "Aklasur Rahman\n\n--\n\nDhaka\n\nAccepter\nIgnorer\nPlus"
    assert detect_connection_state(text) == ("incoming_request", True)
Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/test_scraping.py
Line: 767-789

Comment:
**Missing French `incoming_request` test**

The French detection for `incoming_request` (checking for both `Accepter` and `Ignorer`) has no test coverage. All other French states — `already_connected`, `connectable`, `follow_only`, `pending` — have dedicated tests, but this path is silently skipped.

A missing test here means a regression in this code path could go unnoticed. Consider adding:

```python
def test_incoming_request_fr(self):
    text = "Aklasur Rahman\n\n--\n\nDhaka\n\nAccepter\nIgnorer\nPlus"
    assert detect_connection_state(text) == ("incoming_request", True)
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +70 to +72
top = profile_text[:300]
if "\u00b7 1st" in top or "\u00b7 1er" in top:
return "already_connected", False
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 is_french always False for French already-connected profiles

When "· 1er" is detected (French interface), the function still returns is_french=False. While this is not a functional bug today (the caller returns early for already_connected before consulting is_french), it is semantically inconsistent — a French profile is reported as non-French — and could silently mislead future callers that rely on the flag.

Consider returning True when the French indicator is matched:

Suggested change
top = profile_text[:300]
if "\u00b7 1st" in top or "\u00b7 1er" in top:
return "already_connected", False
top = profile_text[:300]
if "\u00b7 1st" in top:
return "already_connected", False
if "\u00b7 1er" in top:
return "already_connected", True
Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/scraping/connection.py
Line: 70-72

Comment:
**`is_french` always `False` for French already-connected profiles**

When `"· 1er"` is detected (French interface), the function still returns `is_french=False`. While this is not a functional bug today (the caller returns early for `already_connected` before consulting `is_french`), it is semantically inconsistent — a French profile is reported as non-French — and could silently mislead future callers that rely on the flag.

Consider returning `True` when the French indicator is matched:

```suggestion
    top = profile_text[:300]
    if "\u00b7 1st" in top:
        return "already_connected", False
    if "\u00b7 1er" in top:
        return "already_connected", True
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +540 to +542
more_btn = self._page.locator(
"main button[aria-label*='More'], main button[aria-label*='Plus']"
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 aria-label*='Plus' selector is overly broad

The *= CSS attribute substring selector matches any button whose aria-label contains the string "Plus". On a French LinkedIn page, other buttons could incidentally contain "Plus" (e.g., a hypothetical "Ajouter plus de compétences" button with aria-label="Plus de compétences"), causing _open_more_menu to click the wrong element.

LinkedIn's French three-dot button typically carries aria-label="Plus d'options" or aria-label="Plus". A more precise match would reduce the risk:

Suggested change
more_btn = self._page.locator(
"main button[aria-label*='More'], main button[aria-label*='Plus']"
)
more_btn = self._page.locator(
"main button[aria-label*='More'], main button[aria-label='Plus'], main button[aria-label=\"Plus d'options\"]"
)

If the exact aria-label is confirmed through manual testing, tightening this selector would make the code more resilient against layout changes that add new "Plus"-containing buttons.

Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/scraping/extractor.py
Line: 540-542

Comment:
**`aria-label*='Plus'` selector is overly broad**

The `*=` CSS attribute substring selector matches any button whose `aria-label` *contains* the string `"Plus"`. On a French LinkedIn page, other buttons could incidentally contain "Plus" (e.g., a hypothetical "Ajouter plus de compétences" button with `aria-label="Plus de compétences"`), causing `_open_more_menu` to click the wrong element.

LinkedIn's French three-dot button typically carries `aria-label="Plus d'options"` or `aria-label="Plus"`. A more precise match would reduce the risk:

```suggestion
        more_btn = self._page.locator(
            "main button[aria-label*='More'], main button[aria-label='Plus'], main button[aria-label=\"Plus d'options\"]"
        )
```

If the exact aria-label is confirmed through manual testing, tightening this selector would make the code more resilient against layout changes that add new "Plus"-containing buttons.

How can I resolve this? If you propose a fix, please make it concise.

@stickerdaniel
Copy link
Copy Markdown
Owner

Isn't there some a way to force the locale to english through the browser profile? Might be worth checking, this would be a cleaner solution that also resolves issues with other languages than french

Dominien added a commit to Dominien/linkedin-mcp-server that referenced this pull request Apr 13, 2026
…unts

`connect_with_person` currently returns `connect_unavailable` on every
profile for users whose LinkedIn UI is not in English (e.g. German,
French). The root cause sits upstream of the parser: Playwright's
`locale="en-US"` context option (core/browser.py) only drives
`navigator.language` and the Accept-Language header; LinkedIn ignores
both for logged-in sessions and serves the UI in the language chosen
under Settings → Display language on the account. A US-locale browser
thus still renders `Vernetzen` / `Folgen` for a German account,
`Se connecter` / `Suivre` for French, and the current detector — which
only matches the English `Connect` / `Follow` / `Pending` / `Accept`
strings — falls through to `unavailable`.

This change keeps `locale="en-US"` as a hint and adds explicit per-
locale tables so detection works regardless of what LinkedIn actually
renders:

  - `STATE_BUTTON_MAP_BY_LOCALE` — button text to click, keyed by
    locale + state. Ships `en`, `de`, `fr`. New locales = one line.
  - `_DETECTION_LABELS_BY_LOCALE` — labels used while parsing the
    action area.
  - `_SECTION_HEADINGS_BY_LOCALE` — per-locale section headings that
    mark the end of the action area (About / Info / Infos / …).
  - `_FIRST_DEGREE_MARKERS` — `· 1st` / `· 1.` / `· 1er`.
  - `_MORE_ARIA_LABELS` — `More` / `Mehr` / `Plus` for the three-dot
    menu button.

`detect_connection_state` now returns `tuple[ConnectionState, str]`
where the second element is the detected locale, so callers pick the
right button label without needing their own language guess.
`connect_with_person` threads the locale through to
`STATE_BUTTON_MAP_BY_LOCALE[locale][state]`, and `_open_more_menu`
builds its aria-label selector and menu-item regex from
`_MORE_ARIA_LABELS` + `all_button_texts("connectable")` so it works on
any supported locale.

Tests: updated the 9 existing `TestDetectConnectionState` cases for
the new tuple return, added 6 German cases (including an action-area
cut-at-`Info` regression) and 5 French cases (parity with stickerdaniel#319). All
383 tests pass, ruff + ruff-format + ty + pre-commit clean.

This supersedes stickerdaniel#319 (French-only) with a generalized fix; credit to
@vrpctaywal for identifying the French case and the `_contains`
helper shape.
@stickerdaniel
Copy link
Copy Markdown
Owner

Thanks for the PR. Going with #352 instead, which detects the connection state via role/aria structure rather than matching specific locale strings, so it covers all languages at once. Closing this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants