Skip to content
Snippets Groups Projects
Commit 8f404faf authored by Pratik Kamble's avatar Pratik Kamble Committed by Brian Perry
Browse files

Issue #3377803 by pratik_kamble, coby.sher, brianperry: Localization

parent 8a4f1cc5
No related branches found
No related tags found
1 merge request!113377803: Adds feature to fetch localized content.
Pipeline #26282 passed
......@@ -54,6 +54,7 @@ async function main() {
const jsonApiClient = new JsonApiClient(baseUrl, {
customFetch,
cache,
defaultLocale: "en",
});
const recipeCollection = await jsonApiClient.get<
......@@ -62,6 +63,9 @@ async function main() {
console.log("JSON:API Collection", recipeCollection);
const collection = await jsonApiClient.get("node--recipe", { locale: "es" });
console.log("JSON:API Collection", collection);
document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
<pre>${JSON.stringify(recipeCollection, null, 2)}</pre>
`;
......
......@@ -26,6 +26,11 @@ export default class ApiClient {
*/
authentication: ApiClientOptions["authentication"];
/**
* {@link ApiClientOptions.defaultLocale}
*/
defaultLocale: ApiClientOptions["defaultLocale"];
/**
* {@link ApiClientOptions.cache}
*/
......@@ -40,12 +45,14 @@ export default class ApiClient {
if (!baseUrl) {
throw new Error("baseUrl is required");
}
const { apiPrefix, customFetch, authentication, cache } = options || {};
const { apiPrefix, customFetch, authentication, cache, defaultLocale } =
options || {};
this.baseUrl = baseUrl;
this.apiPrefix = apiPrefix;
this.customFetch = customFetch;
this.authentication = authentication;
this.cache = cache;
this.defaultLocale = defaultLocale;
}
/**
......
......@@ -26,6 +26,8 @@ export type ApiClientOptions = {
) => Promise<Response>;
authentication?: Authentication;
defaultLocale?: Locale;
};
type Authentication = {
......@@ -43,3 +45,5 @@ export interface Cache {
get<T>(key: string, ...args: unknown[]): Promise<T>;
set<T>(key: string, value: T, ...args: unknown[]): Promise<unknown>;
}
export type Locale = string;
......@@ -2,6 +2,7 @@ import ApiClient from "../src/ApiClient";
const baseUrl = "https://dev-drupal-api-client-poc.pantheonsite.io";
const apiPrefix = "customprefix";
const defaultLocale = "en";
test("Create default instance of class", () => {
const defaultClient = new ApiClient(baseUrl);
......@@ -15,13 +16,18 @@ test("Create instance of class with options", () => {
get: async <T>() => ({}) as T,
set: async () => {},
};
const optionsClient = new ApiClient(baseUrl, { apiPrefix, cache });
const optionsClient = new ApiClient(baseUrl, {
apiPrefix,
cache,
defaultLocale,
});
expect(optionsClient).toBeInstanceOf(ApiClient);
expect(optionsClient.baseUrl).toBe(baseUrl);
expect(optionsClient.fetch).toBeTypeOf("function");
expect(optionsClient.apiPrefix).toBe(apiPrefix);
expect(optionsClient.cache?.get).toBeDefined();
expect(optionsClient.cache?.set).toBeDefined();
expect(optionsClient.defaultLocale).toBe(defaultLocale);
});
test("ApiClient class can be extended", () => {
......
import ApiClient, { ApiClientOptions, BaseUrl } from "@drupal/api-client";
import ApiClient, {
ApiClientOptions,
BaseUrl,
Locale,
} from "@drupal/api-client";
/**
* JSON:API Client class provides functionality specific to JSON:API server.
......@@ -22,31 +26,37 @@ export default class JsonApiClient extends ApiClient {
* Retrieves data of a specific entity type and bundle from the JSON:API.
* @param type - The type of resource to retrieve, in the format "entityType--bundle".
* For example, "node--page".
* @param options - (Optional) Additional options for customizing the request.
* @returns A Promise that resolves to the JSON data of the requested resource.
*
* @example
* Using JSONAPI.CollectionResourceDoc type from the jsonapi-typescript package
* ```
* const collection = await jsonApiClient.get<JSONAPI.CollectionResourceDoc<string, Recipe>>("node--recipe");
* ```
*/
async get<T>(type: string) {
async get<T>(type: string, options?: { locale?: Locale }) {
const [entityTypeId, bundleId] = type.split("--");
const localeSegment = options?.locale || this.defaultLocale;
const cacheKey = localeSegment
? `${localeSegment}--${entityTypeId}--${bundleId}`
: `${entityTypeId}--${bundleId}`;
if (this.cache) {
const cacheKey = `${entityTypeId}--${bundleId}`;
const cachedResponse = await this.cache.get<T>(cacheKey);
if (cachedResponse) {
return cachedResponse;
}
return this.fetch(
`${this.baseUrl}/${this.apiPrefix}/${entityTypeId}/${bundleId}`,
).then(async (res) => {
const json = (await res.clone().json()) as T;
await this.cache?.set(cacheKey, json);
return res.json() as T;
});
}
return this.fetch(
`${this.baseUrl}/${this.apiPrefix}/${entityTypeId}/${bundleId}`,
).then((response) => response.json() as T);
const apiUrl = `${this.baseUrl}${
localeSegment ? `/${localeSegment}` : ""
}/${this.apiPrefix}/${entityTypeId}/${bundleId}`;
const response = await this.fetch(apiUrl);
const json = (await response.json()) as T;
if (this.cache) {
await this.cache?.set(cacheKey, json);
}
return json;
}
}
import JsonApiClient from "../src/JsonApiClient";
import nodePage from "./mocks/data/node-page.json";
import nodePageEnglish from "./mocks/data/node-page-en.json";
import nodePageSpanish from "./mocks/data/node-page-es.json";
const baseUrl = "https://dev-drupal-api-client-poc.pantheonsite.io";
const defaultLocale = "en";
const overrideLocale = "es";
describe("JsonApiClient", () => {
it("should fetch data for a given type", async () => {
const apiClient = new JsonApiClient(baseUrl);
......@@ -12,4 +18,21 @@ describe("JsonApiClient", () => {
// Assert that the data was fetched correctly
expect(result).toEqual(nodePage);
});
it("should fetch data for a given type with default locale", async () => {
const apiClient = new JsonApiClient(baseUrl, { defaultLocale });
const type = "node--page";
const result = await apiClient.get(type);
// Assert that the data was fetched correctly
expect(result).toEqual(nodePageEnglish);
});
it("should fetch data for a given type with overridden locale", async () => {
const apiClient = new JsonApiClient(baseUrl, { defaultLocale });
const type = "node--page";
const result = await apiClient.get(type, { locale: overrideLocale });
// Assert that the data was fetched correctly
expect(result).toEqual(nodePageSpanish);
});
});
{
"jsonapi": {
"version": "1.0",
"meta": {
"links": {
"self": {
"href": "http://jsonapi.org/format/1.0/"
}
}
}
},
"data": [
{
"type": "node--page",
"id": "f3526241-6d30-4a27-bc63-af1b6d07c219",
"links": {
"self": {
"href": "https://umami-api-client-demo.ddev.site/en/jsonapi/node/page/f3526241-6d30-4a27-bc63-af1b6d07c219?resourceVersion=id%3A38"
}
},
"attributes": {
"drupal_internal__nid": 19,
"drupal_internal__vid": 38,
"langcode": "en",
"revision_timestamp": "2023-09-16T18:38:15+00:00",
"revision_log": null,
"status": true,
"title": "About Umami",
"created": "2023-09-16T18:38:15+00:00",
"changed": "2023-09-16T18:38:15+00:00",
"promote": false,
"sticky": false,
"default_langcode": true,
"revision_translation_affected": null,
"moderation_state": "published",
"path": {
"alias": "/about-umami",
"pid": 103,
"langcode": "en"
},
"content_translation_source": "und",
"content_translation_outdated": false,
"body": {
"value": "<p>Umami is a fictional food magazine that has been created to demonstrate how you might build a Drupal site using functionality provided 'out of the box'.</p><p>For more information visit <a href='https://www.drupal.org/docs/umami-drupal-demonstration-installation-profile'>https://www.drupal.org/docs/umami-drupal-demonstration-installation-profile</a>.</p>",
"format": "basic_html",
"processed": "<p>Umami is a fictional food magazine that has been created to demonstrate how you might build a Drupal site using functionality provided 'out of the box'.</p>\n<p>For more information visit <a href=\"https://www.drupal.org/docs/umami-drupal-demonstration-installation-profile\">https://www.drupal.org/docs/umami-drupal-demonstration-installation-profile</a>.</p>\n",
"summary": null
}
},
"relationships": {
"node_type": {
"data": {
"type": "node_type--node_type",
"id": "2f667892-a78f-433e-8e35-ee3863fc14f5",
"meta": {
"drupal_internal__target_id": "page"
}
},
"links": {
"related": {
"href": "https://umami-api-client-demo.ddev.site/en/jsonapi/node/page/f3526241-6d30-4a27-bc63-af1b6d07c219/node_type?resourceVersion=id%3A38"
},
"self": {
"href": "https://umami-api-client-demo.ddev.site/en/jsonapi/node/page/f3526241-6d30-4a27-bc63-af1b6d07c219/relationships/node_type?resourceVersion=id%3A38"
}
}
},
"revision_uid": {
"data": {
"type": "user--user",
"id": "fb5d838c-554a-4b6b-8785-c61605b4f9c1",
"meta": {
"drupal_internal__target_id": 7
}
},
"links": {
"related": {
"href": "https://umami-api-client-demo.ddev.site/en/jsonapi/node/page/f3526241-6d30-4a27-bc63-af1b6d07c219/revision_uid?resourceVersion=id%3A38"
},
"self": {
"href": "https://umami-api-client-demo.ddev.site/en/jsonapi/node/page/f3526241-6d30-4a27-bc63-af1b6d07c219/relationships/revision_uid?resourceVersion=id%3A38"
}
}
},
"uid": {
"data": {
"type": "user--user",
"id": "fb5d838c-554a-4b6b-8785-c61605b4f9c1",
"meta": {
"drupal_internal__target_id": 7
}
},
"links": {
"related": {
"href": "https://umami-api-client-demo.ddev.site/en/jsonapi/node/page/f3526241-6d30-4a27-bc63-af1b6d07c219/uid?resourceVersion=id%3A38"
},
"self": {
"href": "https://umami-api-client-demo.ddev.site/en/jsonapi/node/page/f3526241-6d30-4a27-bc63-af1b6d07c219/relationships/uid?resourceVersion=id%3A38"
}
}
}
}
}
],
"links": {
"self": {
"href": "https://umami-api-client-demo.ddev.site/en/jsonapi/node/page"
}
}
}
{
"jsonapi": {
"version": "1.0",
"meta": {
"links": {
"self": {
"href": "http://jsonapi.org/format/1.0/"
}
}
}
},
"data": [
{
"type": "node--page",
"id": "f3526241-6d30-4a27-bc63-af1b6d07c219",
"links": {
"self": {
"href": "https://umami-api-client-demo.ddev.site/es/jsonapi/node/page/f3526241-6d30-4a27-bc63-af1b6d07c219?resourceVersion=id%3A38"
}
},
"attributes": {
"drupal_internal__nid": 19,
"drupal_internal__vid": 38,
"langcode": "es",
"revision_timestamp": "2023-09-16T18:38:15+00:00",
"revision_log": null,
"status": true,
"title": "Acerca de Umami",
"created": "2023-09-16T18:38:15+00:00",
"changed": "2023-09-16T18:38:15+00:00",
"promote": false,
"sticky": false,
"default_langcode": false,
"revision_translation_affected": true,
"moderation_state": "published",
"path": {
"alias": "/acerca-de-umami",
"pid": 104,
"langcode": "es"
},
"content_translation_source": "und",
"content_translation_outdated": false,
"body": {
"value": "<p> Umami es una revista ficticia de alimentos que se ha creado para demostrar cómo se puede construir un sitio de Drupal con la funcionalidad que se proporciona 'fuera de la caja'. </p> <p> Para obtener más información, visite <a href='https://www.drupal.org/docs/umami-drupal-demonstration-installation-profile'>https://www.drupal.org/docs/umami-drupal-demonstration-installation-profile</a>.</p> ",
"format": "basic_html",
"processed": "<p> Umami es una revista ficticia de alimentos que se ha creado para demostrar cómo se puede construir un sitio de Drupal con la funcionalidad que se proporciona 'fuera de la caja'. </p>\n<p> Para obtener más información, visite <a href=\"https://www.drupal.org/docs/umami-drupal-demonstration-installation-profile\">https://www.drupal.org/docs/umami-drupal-demonstration-installation-profile</a>.</p>\n",
"summary": null
}
},
"relationships": {
"node_type": {
"data": {
"type": "node_type--node_type",
"id": "2f667892-a78f-433e-8e35-ee3863fc14f5",
"meta": {
"drupal_internal__target_id": "page"
}
},
"links": {
"related": {
"href": "https://umami-api-client-demo.ddev.site/es/jsonapi/node/page/f3526241-6d30-4a27-bc63-af1b6d07c219/node_type?resourceVersion=id%3A38"
},
"self": {
"href": "https://umami-api-client-demo.ddev.site/es/jsonapi/node/page/f3526241-6d30-4a27-bc63-af1b6d07c219/relationships/node_type?resourceVersion=id%3A38"
}
}
},
"revision_uid": {
"data": {
"type": "user--user",
"id": "fb5d838c-554a-4b6b-8785-c61605b4f9c1",
"meta": {
"drupal_internal__target_id": 7
}
},
"links": {
"related": {
"href": "https://umami-api-client-demo.ddev.site/es/jsonapi/node/page/f3526241-6d30-4a27-bc63-af1b6d07c219/revision_uid?resourceVersion=id%3A38"
},
"self": {
"href": "https://umami-api-client-demo.ddev.site/es/jsonapi/node/page/f3526241-6d30-4a27-bc63-af1b6d07c219/relationships/revision_uid?resourceVersion=id%3A38"
}
}
},
"uid": {
"data": {
"type": "user--user",
"id": "fb5d838c-554a-4b6b-8785-c61605b4f9c1",
"meta": {
"drupal_internal__target_id": 7
}
},
"links": {
"related": {
"href": "https://umami-api-client-demo.ddev.site/es/jsonapi/node/page/f3526241-6d30-4a27-bc63-af1b6d07c219/uid?resourceVersion=id%3A38"
},
"self": {
"href": "https://umami-api-client-demo.ddev.site/es/jsonapi/node/page/f3526241-6d30-4a27-bc63-af1b6d07c219/relationships/uid?resourceVersion=id%3A38"
}
}
}
}
}
],
"links": {
"self": {
"href": "https://umami-api-client-demo.ddev.site/jsonapi/node/page"
}
}
}
import { http } from "msw";
import nodePage from "./data/node-page.json";
import nodeRecipe from "./data/node-recipe.json";
import nodePageEnglish from "./data/node-page-en.json";
import nodePageSpanish from "./data/node-page-es.json";
const baseUrl = "https://dev-drupal-api-client-poc.pantheonsite.io";
const apiPrefix = "jsonapi";
const defaultLocale = "en";
const overrideLocale = "es";
export default [
http.get(
`${baseUrl}/${apiPrefix}/node/page`,
......@@ -25,4 +31,22 @@ export default [
headers: request.headers,
}),
),
http.get(
`${baseUrl}/${defaultLocale}/${apiPrefix}/node/page`,
({ request }) =>
new Response(JSON.stringify(nodePageEnglish), {
status: 200,
statusText: "Ok",
headers: request.headers,
}),
),
http.get(
`${baseUrl}/${overrideLocale}/${apiPrefix}/node/page`,
({ request }) =>
new Response(JSON.stringify(nodePageSpanish), {
status: 200,
statusText: "Ok",
headers: request.headers,
}),
),
];
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment