# Localization Module

Client-side localization for country and language selection with cookie persistence. Used in Webflow projects to drive country/locale dropdowns, URL locale prefixes, contact/social data per office, and optional Weglot translation integration.

This repo also includes a country-specific homepage schema helper script, `country-home-schema.js`, for generating JSON-LD on country home pages from CMS or page-level data attributes.

## Overview

- **Country & language selection** via dropdowns; selection is stored in a cookie and in memory.
- **URL locale support**: Paths can use a locale prefix (e.g. `/de/`, `/pt-br/`). English can stay at root (no `/en`).
- **Persistence**: Cookie stores country name/code/slug, language list, selected language, SpendVue flag, and translator language code.
- **Redirects**: Switching country can redirect (e.g. `/contact` ↔ `/office/{slug}`, home, or locale-prefixed URLs).
- **Contact & social data**: Footer company/address and social links are loaded from the appropriate source page (International → `/contact`, others → `/office/{country-slug}`) and applied to targets on the current page.
- **SpendVue**: Optional visibility toggles via `[spend-vue=true]` / `[spend-vue=false]`.
- **Weglot**: Optional sync of selected language with the Weglot widget.
- **Greek**: When Greek is active, a CSS custom property is set so headings use the body font (e.g. for missing glyphs).
- **First-visit modal**: Optional localization modal can show once per browser/session state and be dismissed permanently.

---

## Stored Data (Cookie)

Cookie name: `localization`. Stored as JSON with:

| Field                          | Description                                                                      |
| ------------------------------ | -------------------------------------------------------------------------------- |
| `countryName`                  | Display name (e.g. `"International"`, `"United Kingdom"`)                        |
| `countryCode`                  | Code (e.g. `INT`, `GB`)                                                          |
| `countrySlug`                  | URL slug for country (e.g. `united-kingdom`)                                     |
| `languages`                    | Array of language names for the selected country                                 |
| `isSpendVue`                   | `true` if the selected locale has SpendVue                                       |
| `selectedLanguage`             | Display name of selected language                                                |
| `selectedLanguageAbbreviation` | BCP-style code (e.g. `en`, `pt-br`)                                              |
| `selectedLanguageConveyThis`   | Legacy cookie field reused to store the translator language code for widget sync |

---

## DOM Attributes Reference

The script uses data attributes to find and update elements. Add these in Webflow (or your CMS) so the script can bind correctly.

### Country home schema

The `country-home-schema.js` script generates the base JSON-LD for country-specific home pages and fills it from page data attributes. If a script element with `id="country-home-schema"` is not already present, the script creates one in `<head>` automatically.

| Selector / Attribute       | Purpose                                                                                                                       |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `#country-home-schema`     | JSON-LD script element written by the script. It may already exist, but it no longer needs to be added manually to each page. |
| `data-schema-country-name` | Country display name used for the page name and `areaServed.name`.                                                            |
| `data-schema-name`         | Office/legal name used for `mainEntity.name`.                                                                                 |
| `data-schema-email`        | Office email used for `mainEntity.email`.                                                                                     |
| `data-schema-phone`        | Office phone used for `mainEntity.telephone`.                                                                                 |
| `data-schema-address`      | Single-line address used as `mainEntity.address.streetAddress`.                                                               |
| `data-schema-linkedin`     | LinkedIn URL added to `mainEntity.sameAs`.                                                                                    |
| `data-schema-x`            | X URL added to `mainEntity.sameAs`.                                                                                           |
| `data-schema-vimeo`        | Vimeo URL added to `mainEntity.sameAs`.                                                                                       |
| `data-schema-youtube`      | YouTube URL added to `mainEntity.sameAs`.                                                                                     |
| `data-schema-instagram`    | Instagram URL added to `mainEntity.sameAs`.                                                                                   |

Behavior summary:

- Reads the first element found for each `data-schema-*` attribute.
- Creates a base `WebPage` schema if `#country-home-schema` is missing.
- Updates the root schema name to `ERA Group {Country}` when a country name is present.
- Sets `schema.url` from the current page path.
- Ensures `mainEntity` exists as a `LocalBusiness`.
- Ensures `mainEntity.address` exists as a `PostalAddress`.
- Ensures `mainEntity.areaServed` exists as a `Country`.
- Ensures `mainEntity.parentOrganization` exists as:

```json
{
  "@type": "Organization",
  "@id": "/#organization",
  "name": "E R Associates (Europe) Ltd",
  "alternateName": "ERA Group"
}
```

- Removes empty values and removes empty nested objects before writing the JSON-LD back.

### Country / locale

| Selector / Attribute          | Purpose                                                                                                 |
| ----------------------------- | ------------------------------------------------------------------------------------------------------- |
| `[localization=locale]`       | One per country/locale option. Wraps the clickable area (e.g. dropdown item).                           |
| `localization-country`        | On the locale element. Country display name (e.g. `International`, `Austria`).                          |
| `localization-country-code`   | On the locale element. Country code (e.g. `INT`, `AT`).                                                 |
| `localization-country-slug`   | Optional but preferred on the locale element. Canonical country slug (e.g. `united-kingdom`, `mexico`). |
| `[localization=country-code]` | Element whose `textContent` is set to the current country code (e.g. in nav).                           |

Inside each `[localization=locale]` you can have:

- `localization-country-slug` is preferred for `countrySlug` resolution. If it is missing, the script falls back to a country link such as `<a href="/countries/austria" fs-list-element="item-link">` or `a[href^="/countries/"]`.
- Language items: `[localization-language]` with value = language name (e.g. `English`, `German`).
- Optional: `[localization-sv=true]` to mark this locale as SpendVue.
- Optional: `localization-language-abbreviation` or `data-language-abbreviation` on language elements for the BCP code.

### Language list (dropdown)

| Selector / Attribute           | Purpose                                                                                                                 |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| `[localization-language=list]` | Container for the list of language options (e.g. dropdown list).                                                        |
| `[localization-language]`      | Each language option. Attribute value = language name (e.g. `English`). Skip value `list` (reserved for the container). |
| `[localization=language]`      | Element whose `textContent` is set to the current language label (e.g. dropdown toggle text).                           |

Optional on a language item:

- Inner `div` or text — can be used as the display abbreviation (e.g. `EN`, `DE`).

### First-visit localization modal

| Selector / Attribute               | Purpose                                                                                       |
| ---------------------------------- | --------------------------------------------------------------------------------------------- |
| `[loc-modal-element="modal-loc"]`  | Modal root. Script shows it with `display: flex` and hides it with `display: none`.           |
| `[loc-modal-element="close-loc"]`  | Close trigger(s) inside modal. All matching elements are wired (overlay, close button, etc.). |
| `[localization=full-country-name]` | Heading text target populated with current full country name or `International`.              |

### SpendVue visibility

| Selector            | Behavior                                                  |
| ------------------- | --------------------------------------------------------- |
| `[spend-vue=true]`  | Shown when current country is SpendVue; hidden otherwise. |
| `[spend-vue=false]` | Hidden when current country is SpendVue; shown otherwise. |

### Contact data (footer)

Source content is read from another page and injected into targets on the current page.

| Attribute                       | Role                                                                                                          |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `[contact-data=company-source]` | On the **source** page (e.g. `/contact` or `/office/{slug}`). Wrapper whose `innerHTML` is the company block. |
| `[contact-data=address-source]` | On the **source** page. Wrapper whose `innerHTML` is the address block.                                       |
| `[contact-data=company-target]` | On the **current** page. Element that receives the company HTML.                                              |
| `[contact-data=address-target]` | On the **current** page. Element that receives the address HTML.                                              |

### Social links (footer)

| Attribute                        | Role                                                                          |
| -------------------------------- | ----------------------------------------------------------------------------- |
| `[social-data=linkedin-source]`  | On the **source** page. Element (or link inside it) holding the LinkedIn URL. |
| `[social-data=instagram-source]` | Same for Instagram.                                                           |
| `[social-data=youtube-source]`   | Same for YouTube.                                                             |
| `[social-data=x-source]`         | Same for X (Twitter).                                                         |
| `[social-data=vimeo-source]`     | Same for Vimeo.                                                               |
| `[social-data=linkedin-target]`  | On the **current** page. Link to update with LinkedIn `href`.                 |
| `[social-data=instagram-target]` | Same for Instagram.                                                           |
| `[social-data=youtube-target]`   | Same for YouTube.                                                             |
| `[social-data=x-target]`         | Same for X.                                                                   |
| `[social-data=vimeo-target]`     | Same for Vimeo.                                                               |

### CMS list reorder and limit

| Selector / Attribute           | Purpose                                                                                                                                                              |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `[localization=reorder-list]`  | List container (e.g. CMS list). Items are reordered so entries matching the current country appear first.                                                            |
| `localization-limit`           | Optional, on the same list. Integer. After reorder, only this many items are shown; the rest are set to `display: none`.                                             |
| `localization-visible-display` | Optional, on the list or individual item. Controls which inline `display` value is applied when an item should be visible, useful if items start hidden in site CSS. |
| `[fs-list-field="country"]`    | On each list item. Element(s) whose text is the country name; used to match the current country.                                                                     |

List items are detected as `[role="listitem"]` or `.w-dyn-item`, including nested containers such as `.swiper-wrapper`. If the list lives inside a Swiper, the script refreshes the Swiper after reordering. Visible items get an explicit inline display value so lists can safely start hidden in CSS. The default visible display is `flex`, unless overridden by `localization-visible-display`. If `localization-limit` is missing or invalid, all items are shown. Items without readable `[fs-list-field="country"]` text are still shown and keep their relative order after the known-country items. The reorder logic also rechecks briefly after the first pass so late CMS/nested content can be corrected without forcing unnecessary DOM changes every poll.

### Form select (country)

Country select fields (e.g. on contact forms) can be synced with the current country. The script looks for elements that match selectors like `#country input[type="checkbox"][fs-list-field="country"]` and updates selection to match stored country.

---

## URL and locale behavior

- **Locale prefix**: First path segment matching `aa`, `aa-bb`, or `aa-123` is treated as the locale prefix. In this project these values are translator-defined language codes from `localization-language-abbreviation`, not generic country-region guesses.
- **Helpers**: The script uses internal helpers to get/strip/add locale prefix and to build “country home” paths with the preferred language prefix (e.g. `getCountryHomePath(slug)`). English can be normalized to no prefix (e.g. `/en/` → `/`).
- **Redirects on country/language change**:
  - On `/office/{slug}`: switching country goes to the new `/office/{new-slug}` or to `/contact` for International.
  - On `/contact`: switching to a non-International country goes to `/office/{slug}`.
  - On a real country home path, switching country goes to the new country home.
  - On regular non-home pages, switching country keeps the same page path and updates only the locale prefix when needed.
- **Direct root-home visits**: When `/` is visited without a locale prefix and the stored cookie already points to a non-International country, the script redirects to that country home page.
- **URL-first default order** (when stored country is missing/empty): explicit country from URL path (`/{country}` or `/office/{country}`) -> matching country from supported country-filter query params on `/consultants`, `/case-studies`, `/insights` -> locale-prefix match (/hu/...) -> International fallback.
- **Canonical locale defaults**: When the same attribute-defined locale matches multiple countries, the script can apply a JS-side canonical default. Latin America language entries default to Mexico when the Mexico item uses that same attribute code.
- **International language pinning**: For automatic or manual International selection, language is forced to English (United States) (en).

---

## Weglot integration

- Selected language can be synced to the Weglot widget. Session state is used to avoid redundant sync.
- If no explicit Weglot code is provided, the script falls back to the language abbreviation (`localization-language-abbreviation` or `data-language-abbreviation`) before considering legacy visible text.

---

## Greek and fonts

When the active language is Greek (by name, abbreviation, or URL locale), the script sets a CSS variable (e.g. `--_typography---font-styles--heading`) to the body font so headings don’t use a font that lacks Greek glyphs.

---

## Initialization and observers

- **DOMContentLoaded**: The main entry point runs on `DOMContentLoaded` and:
  - Loads from cookie into `LocalizationData`.
  - Applies URL-locale override when the path has a locale and stored state is International or doesn’t match.
  - If no stored data or no country, auto-selects from URL first and then falls back to International with retries.
  - Uses URL-first default resolution before International fallback (locale prefix first, then country-home slug).
  - Enforces English (United States) when country is International.
  - Initializes first-visit localization modal behavior when modal attributes are present.
  - Applies stored data to UI (country code, language list filter, SpendVue visibility, Greek font, Weglot sync).
  - Sets up click delegation for `[localization=locale]` and `[localization-language]`, and handlers for locale-aware navigation (home, contact, etc.).
  - Runs reorder for `[localization=reorder-list]`, contact/social sync, and country select sync.
- **MutationObserver**: A observer watches the nav dropdown container (e.g. `.nav_dropdowns`) for child/subtree changes and reapplies stored data when localization elements appear (e.g. after CMS or dynamic UI loads).
- **Cookie verification**: A timer periodically checks that the cookie and in-memory data are in sync and restores the cookie from memory if it was lost.

---

## Event handling (summary)

- **Clicks on `[localization=locale]`**: Extract country/languages/SpendVue from that element, save to cookie and memory, run redirect logic (office/contact/home/locale), then apply UI updates, contact/social sync, reorder, and Weglot sync. Dropdown is closed after a short delay.
- **International locale click behavior**: International selection forces language to English (United States) instead of selecting the first visible language item.
- **Clicks on language items** (inside `[localization-language=list]`): Update selected language and abbreviation (and Weglot code), persist, optionally redirect to locale-prefixed URL, update language display and Weglot.
- **Home / contact / locale-aware links**: Handlers can redirect to the appropriate locale or country path before navigation.
- **Modal dismissal behavior**: Clicking any [loc-modal-element="close-loc"] or selecting a locale inside the modal dismisses it and stores a one-time dismissal flag.

---

## Dependencies and environment

- **Browser**: Relies on DOM, `document.cookie`, `sessionStorage`, and optional Weglot/Webflow/FinSweet globals.
- **Webflow**: Uses classes like `.w-dropdown`, `.w-dropdown-toggle`, `.w-dyn-item`, `.nav_dropdowns`, `.nav_dropdown-link` where applicable.
- **FinSweet**: Can use `fs-list-element`, `fs-list-field` for CMS-driven links and country fields. Reorder and retry logic account for delayed CMS rendering.

---

## Build and usage

This repo builds every file in `src/files` (for example via `pnpm build` or `pnpm prod`). The output is intended to be loaded in Webflow (for example from the `prod` folder or via CDN).

Include the built `localization.js` script on pages that use the localization attributes above so that country/language selection, redirects, contact/social sync, and reorder/list limits work as described.

Include the built `country-home-schema.js` script on country home pages that contain:

- page elements carrying the `data-schema-*` attributes listed above

Important:

- Files inside `src/files` must be plain JavaScript or CSS. Do not wrap `country-home-schema.js` in `<script>...</script>` tags.
- You can remove the inline `#country-home-schema` JSON-LD block from individual country pages; the script now creates and populates it centrally.
- The local dev server serves built files from `dev/`, not directly from `src/files`.

---

## Recent Updates

- **Locale-aware navigation**: Generic same-origin page links are intercepted and rewritten to the stored locale prefix before navigation (with specialized handlers still taking precedence for home/contact).
- **Faster redirect handling**: Redirect checks run in capture-phase click handling to reduce delay and avoid intermediate hops caused by other listeners.
- **Missing locale fallback on load**: If a non-English locale is stored and an unprefixed URL loads (for example `/case-studies`), the script can redirect to the localized equivalent (`/hu/case-studies`).
- **Stored home redirect on root visits**: Visiting the bare root domain (`/`) now reuses the stored country cookie and redirects to the matching country home instead of staying on the International homepage.
- **Attribute-first locale resolution**: Country matching now prefers `localization-country-slug`, reads URL locale codes from `localization-language-abbreviation`, and keeps Latin America locale-only first visits defaulting to Mexico without hard-coded language codes.
- **First-visit URL hint hardening**: First-load country selection now also respects explicit country slugs in localized paths and supported country-filter query params when they match the current locale, and filtered content pages reload on same-locale country switches so the country prefilter can reset cleanly.
- **Contact/office redirect hardening**: Contact and office redirects now use stronger language/country fallback resolution to reduce two-step `/office/...` then `/{locale}/office/...` behavior.
- **Consultants prefilter behavior**: Stored non-International country filter is pre-applied on each page visit; users can still clear/change filters during the visit.
- **Case studies/insights prefilter behavior**: Same as consultants — pre-applied on each visit from stored country, but not force-reapplied after user interaction on that same load.
- **URL-first default country selection**: First-load default selection now checks locale-prefixed URLs and country-home slugs before any International fallback.
- **International language hardening**: International selection now always stores/uses English (United States) (en) for both automatic and manual International selection.
- **Localization modal support**: Added one-time modal support via `loc-modal-element="modal-loc"` with multi-element close handling (`loc-modal-element="close-loc"`) and dynamic heading country text (`[localization=full-country-name]`).
- **Country home schema support**: `country-home-schema.js` now creates the base JSON-LD centrally, fills it from `data-schema-*` attributes, references `/#organization`, and removes empty schema fields before output.
- **CMS reorder hardening**: Localized CMS reorder now supports nested slider/list containers, defaults visible items to `flex`, shows all items when no valid `localization-limit` is set, and performs short follow-up checks for late nested CMS data without reordering unnecessarily on every poll.
- **Country-switch redirect narrowing**: Top-level static pages are no longer treated as country home pages unless the path slug matches a known country slug, so regular pages keep their path and only switch locale when appropriate.
