From 76b0a3f513173477fe2e21599d28f1296288e50b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nicol=C3=B2=20Pretto?= <info@npretto.com>
Date: Thu, 29 Aug 2024 15:14:41 +0200
Subject: [PATCH] locale query string support on public links and static embeds
 (#47186)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* keep locale in url query params

* e2e tests

* ugly fix for the missing baseUrl error

* applies suggestion from Kelvin to make code more demure

* Update e2e/test/scenarios/sharing/public-question.cy.spec.js

Co-authored-by: Anton Kulyk <kuliks.anton@gmail.com>

* remove it.skip added by mistake

* sort imports

* handle native question/ SyncedParametersList too

* shorter and more accurate comment 😅

* Update e2e/support/helpers/e2e-embedding-helpers.js

Co-authored-by: Mahatthana (Kelvin) Nomsawadi <me@bboykelvin.dev>

---------

Co-authored-by: Anton Kulyk <kuliks.anton@gmail.com>
Co-authored-by: Mahatthana (Kelvin) Nomsawadi <me@bboykelvin.dev>
---
 e2e/support/helpers/e2e-embedding-helpers.js  | 10 +++--
 .../embedding/embedding-dashboard.cy.spec.js  | 28 ++++++++++++++
 .../embedding/embedding-questions.cy.spec.js  |  7 +++-
 .../sharing/public-dashboard.cy.spec.js       | 12 ++++++
 .../sharing/public-question.cy.spec.js        | 38 +++++++++++++++++++
 .../hooks/use-dashboard-url-query.ts          |  2 +-
 .../components/SyncedParametersList.tsx       |  2 +-
 7 files changed, 92 insertions(+), 7 deletions(-)

diff --git a/e2e/support/helpers/e2e-embedding-helpers.js b/e2e/support/helpers/e2e-embedding-helpers.js
index cb3ad1254bd..bcfb8533411 100644
--- a/e2e/support/helpers/e2e-embedding-helpers.js
+++ b/e2e/support/helpers/e2e-embedding-helpers.js
@@ -29,7 +29,11 @@ import { openSharingMenu } from "./e2e-sharing-helpers";
  * Programmatically generate token and visit the embedded page for a question or a dashboard
  *
  * @param {EmbedPayload} payload - The {@link EmbedPayload} we pass to this function
- * @param {{[setFilters]: object, pageStyle: PageStyle, [hideFilters]: string[]}} options
+ * @param {*} options
+ * @param {object} [options.setFilters]
+ * @param {PageStyle} options.pageStyle
+ * @param {string[]} [options.hideFilters]
+ * @param {object} [options.qs]
  *
  * @example
  * visitEmbeddedPage(payload, {
@@ -40,7 +44,7 @@ import { openSharingMenu } from "./e2e-sharing-helpers";
  */
 export function visitEmbeddedPage(
   payload,
-  { setFilters = {}, hideFilters = [], pageStyle = {}, onBeforeLoad } = {},
+  { setFilters = {}, hideFilters = [], pageStyle = {}, onBeforeLoad, qs } = {},
 ) {
   const jwtSignLocation = "e2e/support/external/e2e-jwt-sign.js";
 
@@ -63,7 +67,7 @@ export function visitEmbeddedPage(
 
     cy.visit({
       url: urlRoot,
-      qs: setFilters,
+      qs: { ...setFilters, ...qs },
       onBeforeLoad: window => {
         onBeforeLoad?.(window);
         if (urlHash) {
diff --git a/e2e/test/scenarios/embedding/embedding-dashboard.cy.spec.js b/e2e/test/scenarios/embedding/embedding-dashboard.cy.spec.js
index 55fea4188f4..8a94c8d310c 100644
--- a/e2e/test/scenarios/embedding/embedding-dashboard.cy.spec.js
+++ b/e2e/test/scenarios/embedding/embedding-dashboard.cy.spec.js
@@ -1,4 +1,5 @@
 import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
+import { ORDERS_DASHBOARD_ID } from "e2e/support/cypress_sample_instance_data";
 import {
   addOrUpdateDashboardCard,
   assertEmbeddingParameter,
@@ -16,6 +17,7 @@ import {
   getIframeUrl,
   getRequiredToggle,
   goToTab,
+  main,
   modal,
   multiAutocompleteInput,
   openStaticEmbeddingModal,
@@ -646,7 +648,12 @@ describe("scenarios > embedding > dashboard parameters with defaults", () => {
 });
 
 describeEE("scenarios > embedding > dashboard appearance", () => {
+  const originalBaseUrl = Cypress.config("baseUrl");
   beforeEach(() => {
+    // Reset the baseUrl to the default value
+    // needed because we do `Cypress.config("baseUrl", null);` in the iframe test
+    Cypress.config("baseUrl", originalBaseUrl);
+
     restore();
     cy.signInAsAdmin();
     setTokenFeatures("all");
@@ -939,6 +946,27 @@ describeEE("scenarios > embedding > dashboard appearance", () => {
       expect(iframe.clientHeight).to.be.greaterThan(1000);
     });
   });
+
+  it("should allow to set locale from the `locale` query parameter", () => {
+    cy.request("PUT", `/api/dashboard/${ORDERS_DASHBOARD_ID}`, {
+      enable_embedding: true,
+    });
+    cy.signOut();
+
+    visitEmbeddedPage(
+      {
+        resource: { dashboard: ORDERS_DASHBOARD_ID },
+        params: {},
+      },
+      { qs: { locale: "de" } },
+    );
+
+    main().findByText("Februar 11, 2025, 9:40 PM");
+    // eslint-disable-next-line no-unscoped-text-selectors -- we don't care where the text is
+    cy.findByText("exportieren", { exact: false });
+
+    cy.url().should("include", "locale=de");
+  });
 });
 
 function openFilterOptions(name) {
diff --git a/e2e/test/scenarios/embedding/embedding-questions.cy.spec.js b/e2e/test/scenarios/embedding/embedding-questions.cy.spec.js
index bb321cda3fc..8570be2da34 100644
--- a/e2e/test/scenarios/embedding/embedding-questions.cy.spec.js
+++ b/e2e/test/scenarios/embedding/embedding-questions.cy.spec.js
@@ -6,6 +6,7 @@ import {
   describeEE,
   echartsContainer,
   filterWidget,
+  main,
   openStaticEmbeddingModal,
   popover,
   restore,
@@ -218,8 +219,10 @@ describe("scenarios > embedding > questions", () => {
       });
     });
 
-    // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
-    cy.findByText("Februar 11, 2025, 9:40 PM");
+    main().findByText("Februar 11, 2025, 9:40 PM");
+    main().findByText("Zeilen", { exact: false });
+
+    cy.url().should("include", "locale=de");
   });
 });
 
diff --git a/e2e/test/scenarios/sharing/public-dashboard.cy.spec.js b/e2e/test/scenarios/sharing/public-dashboard.cy.spec.js
index a3d17693926..bfcc23b37d2 100644
--- a/e2e/test/scenarios/sharing/public-dashboard.cy.spec.js
+++ b/e2e/test/scenarios/sharing/public-dashboard.cy.spec.js
@@ -263,6 +263,18 @@ describe("scenarios > public > dashboard", () => {
 
     filterWidget().findByText("002").should("be.visible");
   });
+
+  it("should allow to set locale from the `locale` query parameter", () => {
+    cy.get("@dashboardId").then(id => {
+      visitPublicDashboard(id, {
+        params: { locale: "de" },
+      });
+    });
+
+    // eslint-disable-next-line no-unscoped-text-selectors -- we don't care where the text is
+    cy.findByText("Registerkarte als PDF exportieren").should("be.visible");
+    cy.url().should("include", "locale=de");
+  });
 });
 
 describeEE("scenarios [EE] > public > dashboard", () => {
diff --git a/e2e/test/scenarios/sharing/public-question.cy.spec.js b/e2e/test/scenarios/sharing/public-question.cy.spec.js
index e884f9cae44..4e17f39a359 100644
--- a/e2e/test/scenarios/sharing/public-question.cy.spec.js
+++ b/e2e/test/scenarios/sharing/public-question.cy.spec.js
@@ -1,9 +1,11 @@
 import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
 import {
   assertSheetRowsCount,
+  createNativeQuestion,
   createPublicQuestionLink,
   downloadAndAssert,
   filterWidget,
+  main,
   modal,
   openNativeEditor,
   openNewPublicLinkDropdown,
@@ -194,6 +196,42 @@ describe("scenarios > public > question", () => {
       });
     });
   });
+
+  it("should allow to set locale from the `locale` query parameter", () => {
+    createNativeQuestion(
+      {
+        name: "Native question with a parameter",
+        native: {
+          query:
+            "select '2025-2-11'::DATE as date, {{some_parameter}} as some_parameter ",
+          "template-tags": {
+            some_parameter: {
+              type: "text",
+              name: "some_parameter",
+              id: "1e0806a0-155b-4e24-80bc-c050720201d0",
+              "display-name": "Some Parameter",
+              default: "some default value",
+            },
+          },
+        },
+      },
+      { wrapId: true },
+    );
+
+    cy.get("@questionId").then(id => {
+      cy.request("POST", `/api/card/${id}/public_link`).then(
+        ({ body: { uuid } }) => {
+          cy.visit(
+            `/public/question/${uuid}?locale=de&some_parameter=some_value`,
+          );
+        },
+      );
+    });
+
+    main().findByText("Februar 11, 2025");
+
+    cy.url().should("include", "locale=de");
+  });
 });
 
 const visitPublicURL = () => {
diff --git a/frontend/src/metabase/dashboard/hooks/use-dashboard-url-query.ts b/frontend/src/metabase/dashboard/hooks/use-dashboard-url-query.ts
index 8f07102d837..232c0246b73 100644
--- a/frontend/src/metabase/dashboard/hooks/use-dashboard-url-query.ts
+++ b/frontend/src/metabase/dashboard/hooks/use-dashboard-url-query.ts
@@ -122,7 +122,7 @@ export function useDashboardUrlQuery(
   }, [router, location, selectedTab, dispatch]);
 }
 
-const QUERY_PARAMS_ALLOW_LIST = ["objectId"];
+const QUERY_PARAMS_ALLOW_LIST = ["objectId", "locale"];
 
 function parseTabId(location: Location) {
   const slug = location.query?.tab;
diff --git a/frontend/src/metabase/query_builder/components/SyncedParametersList.tsx b/frontend/src/metabase/query_builder/components/SyncedParametersList.tsx
index da2bdfbd49a..62a53441a7f 100644
--- a/frontend/src/metabase/query_builder/components/SyncedParametersList.tsx
+++ b/frontend/src/metabase/query_builder/components/SyncedParametersList.tsx
@@ -75,7 +75,7 @@ export const SyncedParametersList = ({
   );
 };
 
-const QUERY_PARAMS_ALLOW_LIST = ["objectId"];
+const QUERY_PARAMS_ALLOW_LIST = ["objectId", "locale"];
 
 function buildSearchString(object: Record<string, any>) {
   const currentSearchParams = querystring.parse(
-- 
GitLab