Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamically change the language of Loader #210

Open
mcAnastasiou opened this issue Mar 18, 2021 · 3 comments
Open

Dynamically change the language of Loader #210

mcAnastasiou opened this issue Mar 18, 2021 · 3 comments
Labels
priority: p3 Desirable enhancement or fix. May not be included in next release. status: blocked Resolving the issue is dependent on other work. type: feature request ‘Nice-to-have’ improvement, new feature or different behavior or design.

Comments

@mcAnastasiou
Copy link

Environment details

  1. google.maps.places.AutocompleteService
  2. "@googlemaps/js-api-loader": "^1.11.2",

Steps to reproduce

I load the library as follows

    new Loader({
        apiKey: process.env.REACT_APP_GOOGLE_PLACES_API_KEY,
        libraries: ['places'],
        language: selectedLanguage,
    });

I can dynamically change the language through a dropdown. If i try to reload the library i get an error that says i cannot use the same api key with different params

How can i dynamically change the results of the autocomplete to the desired language?

@jpoehnelt jpoehnelt added priority: p3 Desirable enhancement or fix. May not be included in next release. status: blocked Resolving the issue is dependent on other work. type: feature request ‘Nice-to-have’ improvement, new feature or different behavior or design. labels Mar 19, 2021
@jpoehnelt
Copy link
Contributor

Similar to #5, it is not possible to currently change the language after the script has been loaded. There is a very old request for if you want to add your particular use case at https://issuetracker.google.com/35819089. I am hoping to address #5 this year. Language would be a potential followup to that.

In the meantime, I might investigate #100 to allow reloading the entire API, but that might have billing consequences...

Sorry I don't have better news!

@HaNdTriX
Copy link

HaNdTriX commented Nov 22, 2024

If anyone is looking for a workaround:

Concept

Screenshot 2024-11-22 at 16 49 42

We can actually create a dynamic iframe and load the google maps api in there. Then we can access the api via iframe.contentWindow.google.

If we want to destroy the api we can just remove the iframe from the document. This allows us to instantiate the api multiple times with different configurations.

In the figure above we are loading the sdk twice (de/en) and then create two maps with different languages that are attached to the same DOM.

Code

// https://developers.google.com/maps/documentation/javascript/libraries#libraries-for-dynamic-library-import
type Library =
  | 'core'
  | 'maps'
  | 'places'
  | 'geocoding'
  | 'routes'
  | 'marker'
  | 'geometry'
  | 'elevation'
  | 'streetView'
  | 'journeySharing'
  | 'drawing'
  | 'visualization'

type Libraries = Library[]

export type Config = {
  /**
   * See https://developers.google.com/maps/documentation/javascript/get-api-key.
   */
  apiKey: string
  /**
   * In your application you can specify release channels or version numbers:
   *
   * The weekly version is specified with `version=weekly`. This version is
   * updated once per week, and is the most current.
   *
   * ```
   * const loader = googleMapsApiLoader({apiKey, version: 'weekly'});
   * ```
   *
   * The quarterly version is specified with `version=quarterly`. This version
   * is updated once per quarter, and is the most predictable.
   *
   * ```
   * const loader = googleMapsApiLoader({apiKey, version: 'quarterly'});
   * ```
   *
   * The version number is specified with `version=n.nn`. You can choose
   * `version=3.40`, `version=3.39`, or `version=3.38`. Version numbers are
   * updated once per quarter.
   *
   * ```
   * const loader = googleMapsApiLoader({apiKey, version: '3.40'});
   * ```
   *
   * If you do not explicitly specify a version, you will receive the
   * weekly version by default.
   */
  version?: string
  /**
   * When loading the Maps JavaScript API via the URL you may optionally load
   * additional libraries through use of the libraries URL parameter. Libraries
   * are modules of code that provide additional functionality to the main Maps
   * JavaScript API but are not loaded unless you specifically request them.
   *
   * ```
   * const loader = googleMapsApiLoader({
   *  apiKey,
   *  libraries: ['drawing', 'geometry', 'places', 'visualization'],
   * });
   * ```
   *
   * Set the [list of libraries](https://developers.google.com/maps/documentation/javascript/libraries) for more options.
   */
  libraries?: Libraries
  /**
   * By default, the Maps JavaScript API uses the user's preferred language
   * setting as specified in the browser, when displaying textual information
   * such as the names for controls, copyright notices, driving directions and
   * labels on maps. In most cases, it's preferable to respect the browser
   * setting. However, if you want the Maps JavaScript API to ignore the
   * browser's language setting, you can force it to display information in a
   * particular language when loading the Maps JavaScript API code.
   *
   * For example, the following example localizes the language to Japan:
   *
   * ```
   * const loader = googleMapsApiLoader({apiKey, language: 'ja', region: 'JP'});
   * ```
   *
   * See the [list of supported
   * languages](https://developers.google.com/maps/faq#languagesupport). Note
   * that new languages are added often, so this list may not be exhaustive.
   *
   */
  language?: string
  /**
   * When you load the Maps JavaScript API from maps.googleapis.com it applies a
   * default bias for application behavior towards the United States. If you
   * want to alter your application to serve different map tiles or bias the
   * application (such as biasing geocoding results towards the region), you can
   * override this default behavior by adding a region parameter when loading
   * the Maps JavaScript API code.
   *
   * The region parameter accepts Unicode region subtag identifiers which
   * (generally) have a one-to-one mapping to country code Top-Level Domains
   * (ccTLDs). Most Unicode region identifiers are identical to ISO 3166-1
   * codes, with some notable exceptions. For example, Great Britain's ccTLD is
   * "uk" (corresponding to the domain .co.uk) while its region identifier is
   * "GB."
   *
   * For example, the following example localizes the map to the United Kingdom:
   *
   * ```
   * const loader = Loader({apiKey, region: 'GB'});
   * ```
   */
  region?: string
  /**
   * Use a custom url and path to load the Google Maps API script.
   */
  url?: string
  /**
   * Use a cryptographic nonce attribute.
   */
  nonce?: string
  /**
   * Maps JS customers can configure HTTP Referrer Restrictions in the Cloud
   * Console to limit which URLs are allowed to use a particular API Key. By
   * default, these restrictions can be configured to allow only certain paths
   * to use an API Key. If any URL on the same domain or origin may use the API
   * Key, you can set `auth_referrer_policy=origin` to limit the amount of data
   * sent when authorizing requests from the Maps JavaScript API. This is
   * available starting in version 3.46. When this parameter is specified and
   * HTTP Referrer Restrictions are enabled on Cloud Console, Maps JavaScript
   * API will only be able to load if there is an HTTP Referrer Restriction that
   * matches the current website's domain without a path specified.
   */
  authReferrerPolicy?: 'origin'
  /**
   * A signal to abort the request
   * @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
   */
  signal?: AbortSignal
}

/**
 * The offical Google Maps API loader is synchronous and pollutes the global scope.
 * Therefore it is not possible to change it't language or region after it has been loaded.
 * This function creates an isolated iframe and loads the Google Maps API in it.
 * This way the global scope is not polluted and the language and region can be changed
 * by reloading the iframe.
 *
 * @url https://github.com/googlemaps/js-api-loader/issues/210
 */
export default function googleMapsApiLoader(
  config: Config
): Promise<typeof google> {
  return new Promise((resolve, reject) => {
    const iframe = document.createElement('iframe')
    let isAborted = false

    // Handle abort signal
    if (config.signal) {
      if (config.signal.aborted) {
        reject(new DOMException('Aborted', 'AbortError'))
        return
      }

      config.signal.addEventListener('abort', () => {
        isAborted = true
        if (iframe.parentNode) {
          document.body.removeChild(iframe)
        }
        URL.revokeObjectURL(url)
        reject(new DOMException('Aborted', 'AbortError'))
      })
    }

    // Hide the frame
    iframe.style.width = '0px'
    iframe.style.height = '0px'
    iframe.style.border = 'none'
    iframe.setAttribute('class', 'google-maps-api-loader2')

    // Construct the google sdk URL
    const sdkUrl = new URL(
      config.url || 'https://maps.googleapis.com/maps/api/js'
    )
    sdkUrl.searchParams.append('key', config.apiKey)

    if (config.version) {
      sdkUrl.searchParams.append('v', config.version)
    }

    if (config.libraries) {
      sdkUrl.searchParams.append('libraries', config.libraries.join(','))
    }

    if (config.language) {
      sdkUrl.searchParams.append('language', config.language)
    }

    if (config.region) {
      sdkUrl.searchParams.append('region', config.region)
    }

    if (config.authReferrerPolicy) {
      sdkUrl.searchParams.append(
        'auth_referrer_policy',
        config.authReferrerPolicy
      )
    }

    // Disable performance warning. This doesn't apply to our
    // method since our iframe is loaded asnchronously anyway
    sdkUrl.searchParams.append('callback', 'Function.prototype')

    // Create the iframe document. The browser will do the rest (head, body etc.)
    const html = `<script src="${sdkUrl}"${config.nonce ? ` nonce="${config.nonce}"` : ''}></script>`
    const blob = new Blob([html], { type: 'text/html' })
    const url = URL.createObjectURL(blob)

    iframe.onerror = err => {
      if (!isAborted) {
        reject(err)
      }
    }

    iframe.onload = () => {
      if (isAborted) return

      URL.revokeObjectURL(url)

      // Grab the google object from the iframe context and resolve the promise
      // @ts-expect-error - google is not defined yet
      const google = iframe.contentWindow?.google as typeof google | null

      if (google) resolve(google)
      else reject(new Error('Failed to create isolated Google Map'))
    }

    iframe.src = url
    document.body.appendChild(iframe)
  })
}

Usage

const abortControllerEN = new AbortController()
const googleEN = await googleMapsApiLoader({
  apiKey: '<your_api_key>',
  language: 'en',
  signal: abortControllerEN.signal
})

// To release the `googleEN` run: `abortControllerEN.abort()`

const abortControllerDE = new AbortController()
const googleDE = await googleMapsApiLoader({
  apiKey: '<your_api_key>',
  language: 'de',
  signal: abortControllerDE.signal
})

// To release the `googleDE` run: `abortControllerDE.abort()`

This approach allows us to have multiple maps in different languages in the same page.

Caveats

  • Risk of Memory Leaks: Be careful with strong references.
  • Maps Quota: Please note that the SDK is not being reused here.
  • Performance: Since this approach can load multiple SDKs, more resources might be used.

@usefulthink
Copy link
Contributor

Oh wow, that is actually a pretty wild idea. Thanks for sharing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
priority: p3 Desirable enhancement or fix. May not be included in next release. status: blocked Resolving the issue is dependent on other work. type: feature request ‘Nice-to-have’ improvement, new feature or different behavior or design.
Projects
None yet
Development

No branches or pull requests

4 participants