feat: add French locale support for connection state detection#319
feat: add French locale support for connection state detection#319vrpctaywal wants to merge 1 commit intostickerdaniel:mainfrom
Conversation
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 SummaryThis PR adds French locale ( Key changes:
Notable gaps:
Confidence Score: 4/5Safe 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 Important Files Changed
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]
|
| # --- 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 |
There was a problem hiding this 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:
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.| top = profile_text[:300] | ||
| if "\u00b7 1st" in top or "\u00b7 1er" in top: | ||
| return "already_connected", False |
There was a problem hiding this 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:
| 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.| more_btn = self._page.locator( | ||
| "main button[aria-label*='More'], main button[aria-label*='Plus']" | ||
| ) |
There was a problem hiding this 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:
| 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.|
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 |
…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.
|
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. |
Summary
connect_with_person()always returnsconnect_unavailablefor users whose LinkedIn interface is in Frenchdetect_connection_state()only matches English button labels (Connect,Follow,Pending) but French LinkedIn displaysSe connecter,Suivre,En attente_ACTION_AREA_ENDregex only matches English section headings (About,Experience) but French displaysInfos,Expérience,ActivitéChanges
connection.py:detect_connection_state()now returns(state, is_french)tuple, checking both English and French button labels. AddedSTATE_BUTTON_MAP_FRand 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 bothMore/Plusaria-labels andConnect/Se connectermenu items.Test plan
TestDetectConnectionStatetests passconnect_with_personon a French LinkedIn profile withSe connecterbuttonconnect_with_personon a French LinkedIn profile withSuivre+ More menu🤖 Generated with Claude Code