diff --git a/CHANGELOG.md b/CHANGELOG.md
index fd6b8f3b3c..0625b5a832 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -61,6 +61,7 @@ Our versioning strategy is as follows:
* `[sitecore-jss]` GenericFieldValue model is updated to accept Date type ([#1916](https://github.com/Sitecore/jss/pull/1916))
* `[template/node-xmcloud-proxy]` `[sitecore-jss-proxy]` Introduced /api/healthz endpoint ([#1928](https://github.com/Sitecore/jss/pull/1928))
* `[sitecore-jss]` `[sitecore-jss-angular]` Render field metdata chromes in editMode metadata - in edit mode metadata in Pages, angular package field directives will render wrapping `code` elements with field metadata required for editing; ([#1926](https://github.com/Sitecore/jss/pull/1926))
+* `[sitecore-jss]` Added services for Content Tokens `/sitecore/templates/Feature/Experience Accelerator/Content Tokens/Content Token` so we're a step closer to enabling (custom) content replacement tokens in headless development
### 🛠Breaking Change
diff --git a/packages/sitecore-jss-nextjs-typeguards/.eslintrc b/packages/sitecore-jss-nextjs-typeguards/.eslintrc
new file mode 100644
index 0000000000..c8d10a3c52
--- /dev/null
+++ b/packages/sitecore-jss-nextjs-typeguards/.eslintrc
@@ -0,0 +1,7 @@
+{
+ "extends": [
+ "../../.eslintrc",
+ "../../eslint-configs/react",
+ "../../eslint-configs/typescript"
+ ]
+}
diff --git a/packages/sitecore-jss-nextjs-typeguards/.gitignore b/packages/sitecore-jss-nextjs-typeguards/.gitignore
new file mode 100644
index 0000000000..a7c47537dc
--- /dev/null
+++ b/packages/sitecore-jss-nextjs-typeguards/.gitignore
@@ -0,0 +1 @@
+types/
diff --git a/packages/sitecore-jss-nextjs-typeguards/.nycrc b/packages/sitecore-jss-nextjs-typeguards/.nycrc
new file mode 100644
index 0000000000..914900df4a
--- /dev/null
+++ b/packages/sitecore-jss-nextjs-typeguards/.nycrc
@@ -0,0 +1,23 @@
+{
+ "extension": [
+ ".ts",
+ ".tsx"
+ ],
+ "exclude": [
+ "**/index.ts",
+ "**/*.d.ts",
+ "**/*.test.tsx",
+ "**/*.test.ts",
+ "src/tests",
+ "src/test-data",
+ "dist",
+ "**/sharedTypes.ts",
+ "*.js"
+ ],
+ "all": true,
+ "reporter": [
+ "cobertura",
+ "text"
+ ],
+ "require": ["ts-node/register/transpile-only"]
+}
diff --git a/packages/sitecore-jss-nextjs-typeguards/LICENSE.txt b/packages/sitecore-jss-nextjs-typeguards/LICENSE.txt
new file mode 100644
index 0000000000..d645695673
--- /dev/null
+++ b/packages/sitecore-jss-nextjs-typeguards/LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/packages/sitecore-jss-nextjs-typeguards/README.md b/packages/sitecore-jss-nextjs-typeguards/README.md
new file mode 100644
index 0000000000..b0f1d62f83
--- /dev/null
+++ b/packages/sitecore-jss-nextjs-typeguards/README.md
@@ -0,0 +1,11 @@
+# `sitecore-jss-nextjs-typeguards`
+
+> TODO: description
+
+## Usage
+
+```
+const sitecoreJssNextjsTypeguards = require('sitecore-jss-nextjs-typeguards');
+
+// TODO: DEMONSTRATE API
+```
diff --git a/packages/sitecore-jss-nextjs-typeguards/global.d.ts b/packages/sitecore-jss-nextjs-typeguards/global.d.ts
new file mode 100644
index 0000000000..450b1b5b50
--- /dev/null
+++ b/packages/sitecore-jss-nextjs-typeguards/global.d.ts
@@ -0,0 +1,21 @@
+declare module 'sync-disk-cache' {
+ export default import('sync-disk-cache').default;
+
+ export interface CacheInstance {
+ root: string;
+ set(key: string, editingData: string): string;
+ get(key: string): { value: string; isCached?: boolean };
+ remove(key: string): void;
+ }
+}
+
+declare namespace NodeJS {
+ export interface Global {
+ [key: string]: unknown;
+ requestAnimationFrame: (callback: () => void) => void;
+ window: Window;
+ document: Document;
+ navigator: Navigator;
+ HTMLElement: HTMLElement;
+ }
+}
diff --git a/packages/sitecore-jss-nextjs-typeguards/package.json b/packages/sitecore-jss-nextjs-typeguards/package.json
new file mode 100644
index 0000000000..a4977edad6
--- /dev/null
+++ b/packages/sitecore-jss-nextjs-typeguards/package.json
@@ -0,0 +1,93 @@
+{
+ "name": "@sitecore-jss/sitecore-jss-nextjs-typeguards",
+ "version": "22.2.0-canary.60",
+ "main": "dist/cjs/index.js",
+ "module": "dist/esm/index.js",
+ "sideEffects": false,
+ "scripts": {
+ "build": "npm run clean && tsc -p tsconfig.json && tsc -p tsconfig-esm.json",
+ "clean": "del-cli dist types",
+ "lint": "eslint \"./src/**/*.tsx\" \"./src/**/*.ts\"",
+ "test": "mocha --require ts-node/register/transpile-only --require ./src/tests/shim.ts ./src/tests/jsdom-setup.ts ./src/tests/enzyme-setup.ts \"./src/**/*.test.tsx\" --exit",
+ "prepublishOnly": "npm run build",
+ "coverage": "nyc npm test",
+ "generate-docs": "npx typedoc --plugin typedoc-plugin-markdown --readme none --out ../../ref-docs/sitecore-jss-nextjs-typeguards src/index.ts --githubPages false"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "author": {
+ "name": "Sitecore Corporation",
+ "url": "https://jss.sitecore.com"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "license": "Apache-2.0",
+ "homepage": "https://jss.sitecore.com",
+ "bugs": {
+ "url": "https://github.com/sitecore/jss/issues"
+ },
+ "devDependencies": {
+ "@sitecore-cloudsdk/personalize": "^0.3.1",
+ "@types/chai": "^4.3.4",
+ "@types/chai-as-promised": "^7.1.5",
+ "@types/chai-string": "^1.4.2",
+ "@types/enzyme": "^3.10.12",
+ "@types/mocha": "^10.0.1",
+ "@types/node": "~20.14.2",
+ "@types/prop-types": "^15.7.5",
+ "@types/react": "^18.2.22",
+ "@types/react-dom": "^18.0.10",
+ "@types/sinon": "^10.0.13",
+ "@types/sinon-chai": "^3.2.9",
+ "@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
+ "chai": "^4.3.7",
+ "chai-as-promised": "^7.1.1",
+ "chai-string": "^1.5.0",
+ "chalk": "^4.1.2",
+ "cheerio": "1.0.0-rc.12",
+ "cross-fetch": "^3.1.5",
+ "del-cli": "^5.0.0",
+ "enzyme": "^3.11.0",
+ "eslint": "^8.32.0",
+ "eslint-plugin-react": "^7.32.1",
+ "jsdom": "^21.1.0",
+ "mocha": "^10.2.0",
+ "next": "^14.2.7",
+ "nock": "^13.3.0",
+ "nyc": "^15.1.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "sinon": "^15.0.1",
+ "sinon-chai": "^3.7.0",
+ "ts-node": "^10.9.1",
+ "typescript": "~4.9.4"
+ },
+ "peerDependencies": {
+ "@sitecore-cloudsdk/events": "^0.3.1",
+ "@sitecore-cloudsdk/personalize": "^0.3.1",
+ "next": "^14.2.7",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "dependencies": {
+ "@sitecore-jss/sitecore-jss": "22.2.0-canary.60",
+ "@sitecore-jss/sitecore-jss-dev-tools": "22.2.0-canary.60",
+ "@sitecore-jss/sitecore-jss-nextjs": "22.2.0-canary.60",
+ "@sitecore-jss/sitecore-jss-react": "22.2.0-canary.60",
+ "@vercel/kv": "^0.2.1",
+ "prop-types": "^15.8.1",
+ "regex-parser": "^2.2.11",
+ "sync-disk-cache": "^2.1.0"
+ },
+ "description": "",
+ "types": "types/index.d.ts",
+ "gitHead": "2f4820efddf4454eeee58ed1b2cc251969efdf5b",
+ "files": [
+ "dist",
+ "types",
+ "/*.js",
+ "/*.d.ts"
+ ]
+}
diff --git a/packages/sitecore-jss-nextjs-typeguards/src/index.ts b/packages/sitecore-jss-nextjs-typeguards/src/index.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/sitecore-jss-nextjs-typeguards/src/tests/enzyme-setup.ts b/packages/sitecore-jss-nextjs-typeguards/src/tests/enzyme-setup.ts
new file mode 100644
index 0000000000..b40d09a7ae
--- /dev/null
+++ b/packages/sitecore-jss-nextjs-typeguards/src/tests/enzyme-setup.ts
@@ -0,0 +1,18 @@
+import { configure } from 'enzyme';
+import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
+
+configure({ adapter: new Adapter() });
+
+const originalLog = console.error;
+
+// Suppress deprecation error, since there is no react 18 upgrade for Enzyme
+console.error = (msg) => {
+ if (
+ typeof msg === 'string' &&
+ msg.includes('ReactDOM.render is no longer supported in React 18')
+ ) {
+ return;
+ }
+
+ originalLog(msg);
+};
diff --git a/packages/sitecore-jss-nextjs-typeguards/src/tests/jsdom-setup.ts b/packages/sitecore-jss-nextjs-typeguards/src/tests/jsdom-setup.ts
new file mode 100644
index 0000000000..6a81298342
--- /dev/null
+++ b/packages/sitecore-jss-nextjs-typeguards/src/tests/jsdom-setup.ts
@@ -0,0 +1,42 @@
+/* eslint-disable spaced-comment */
+/* eslint-disable @typescript-eslint/triple-slash-reference */
+// https://github.com/airbnb/enzyme/blob/master/docs/guides/jsdom.md
+
+///
+
+// eslint-disable-next-line no-var
+declare var global: NodeJS.Global;
+
+const { JSDOM } = require('jsdom');
+
+const jsdom = new JSDOM('
', {
+ url: 'https://example.com/',
+});
+const jsDomWindow = jsdom.window;
+
+/**
+ * @param {unknown} src
+ * @param {unknown} target
+ */
+function copyProps(src: unknown, target: { [key: string]: unknown }) {
+ const props = Object.getOwnPropertyNames(src)
+ .filter((prop) => typeof target[prop] === 'undefined')
+ .reduce(
+ (result, prop) => ({
+ ...result,
+ [prop]: Object.getOwnPropertyDescriptor(src, prop),
+ }),
+ {}
+ );
+
+ Object.defineProperties(target, props);
+}
+
+global.window = jsDomWindow;
+global.document = jsDomWindow.document;
+global.navigator = {
+ userAgent: 'node.js',
+} as Navigator;
+
+global.HTMLElement = jsDomWindow.HTMLElement; // makes chai "happy" https://github.com/chaijs/chai/issues/1029
+copyProps(jsDomWindow, global);
diff --git a/packages/sitecore-jss-nextjs-typeguards/src/tests/request.ts b/packages/sitecore-jss-nextjs-typeguards/src/tests/request.ts
new file mode 100644
index 0000000000..d98853500f
--- /dev/null
+++ b/packages/sitecore-jss-nextjs-typeguards/src/tests/request.ts
@@ -0,0 +1,8 @@
+// Mock fetch/Request for nextjs middleware
+
+const crossFetch = require('cross-fetch');
+
+global.fetch = crossFetch;
+global.Headers = crossFetch.Headers;
+global.Request = crossFetch.Request;
+global.Response = crossFetch.Response;
diff --git a/packages/sitecore-jss-nextjs-typeguards/src/tests/shim.ts b/packages/sitecore-jss-nextjs-typeguards/src/tests/shim.ts
new file mode 100644
index 0000000000..0de7f631b3
--- /dev/null
+++ b/packages/sitecore-jss-nextjs-typeguards/src/tests/shim.ts
@@ -0,0 +1,13 @@
+/* eslint-disable spaced-comment */
+// React 16 depends on requestAnimationFrame, need a shim for node.js
+// eslint-disable-next-line spaced-comment
+// https://github.com/facebook/jest/issues/4545
+
+///
+
+// eslint-disable-next-line no-var
+declare var global: NodeJS.Global;
+
+global.requestAnimationFrame = (callback: () => void) => {
+ setTimeout(callback, 0);
+};
diff --git a/packages/sitecore-jss-nextjs-typeguards/src/utils/index.ts b/packages/sitecore-jss-nextjs-typeguards/src/utils/index.ts
new file mode 100644
index 0000000000..9ba795fc65
--- /dev/null
+++ b/packages/sitecore-jss-nextjs-typeguards/src/utils/index.ts
@@ -0,0 +1,11 @@
+export {
+ isField,
+ isImage,
+ isImageFieldValue,
+ isItem,
+ isItemArray,
+ isLinkField,
+ isLinkFieldValue,
+ isRichTextField,
+ isTextField,
+} from './utils';
diff --git a/packages/sitecore-jss-nextjs-typeguards/src/utils/utils.test.ts b/packages/sitecore-jss-nextjs-typeguards/src/utils/utils.test.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/sitecore-jss-nextjs-typeguards/src/utils/utils.ts b/packages/sitecore-jss-nextjs-typeguards/src/utils/utils.ts
new file mode 100644
index 0000000000..ee83e1868c
--- /dev/null
+++ b/packages/sitecore-jss-nextjs-typeguards/src/utils/utils.ts
@@ -0,0 +1,151 @@
+import {
+ Field,
+ ImageField,
+ ImageFieldValue,
+ Item,
+ LinkField,
+ LinkFieldValue,
+ RichTextField,
+ TextField,
+} from '@sitecore-jss/sitecore-jss-nextjs';
+
+/**
+ * typeguard for Field
+ * @param {Field | T} [input]
+ * @returns {boolean}
+ */
+export function isField(input: Field | T): input is Field {
+ if (input === null) return false;
+ if (typeof input !== 'object') return false;
+ if (Array.isArray(input)) return false;
+
+ const result =
+ 'value' in (input as Field) &&
+ Object.keys(input as Field).length >= 1 &&
+ Object.keys(input as Field).length <= 2;
+
+ return result;
+}
+
+/**
+ * typeguard for ImageField
+ * @param {ImageField | T} [input]
+ * @returns {boolean}
+ */
+export function isImage(input: ImageField | T): input is ImageField {
+ const imageField = input as ImageField;
+ const result =
+ 'value' in imageField &&
+ isImageFieldValue(imageField.value) &&
+ Object.keys(imageField).length >= 1 &&
+ Object.keys(imageField).length <= 2;
+
+ return result;
+}
+
+/**
+ * typeguard for ImageFieldValue
+ * @param {ImageFieldValue | T} [input]
+ * @returns {boolean}
+ */
+export function isImageFieldValue(
+ input: ImageFieldValue | T
+): input is ImageFieldValue {
+ const imageFieldValue = input as ImageFieldValue;
+ const result = 'src' in imageFieldValue && Object.keys(imageFieldValue).length >= 1;
+
+ return result;
+}
+
+/**
+ * typeguard for Item
+ * @param {Item | T} [input]
+ * @returns {boolean}
+ */
+export function isItem(input: Item | T): input is Item {
+ const item = input as Item;
+ if (input === null) return false;
+ if (typeof input !== 'object') return false;
+ if (Array.isArray(input)) return false;
+
+ const result =
+ 'fields' in item &&
+ 'name' in item &&
+ Object.keys(item).length >= 2 &&
+ Object.keys(item).length <= 5;
+
+ return result;
+}
+
+/**
+ * typeguard for Array-
+ * @param {Item[] | T} [input]
+ * @returns {boolean}
+ */
+export function isItemArray(input: Item[] | T): input is Item[] {
+ if (input === null) return false;
+ if (!Array.isArray(input)) return false;
+
+ return (input as Item[]).every(isItem);
+}
+
+/**
+ * typeguard for LinkFields.
+ * @param {LinkField | T} [input]
+ * @returns {boolean}
+ */
+export function isLinkField(input: LinkField | T): input is LinkField {
+ const linkField = input as LinkField;
+ const result =
+ 'value' in linkField &&
+ isLinkFieldValue(linkField.value) &&
+ Object.keys(linkField).length >= 1 &&
+ Object.keys(linkField).length <= 3;
+
+ return result;
+}
+
+/**
+ * typeguard for LinkFieldValue
+ * @param {LinkFieldValue | T} [input]
+ * @returns {boolean}
+ */
+export function isLinkFieldValue(
+ input: LinkFieldValue | T
+): input is LinkFieldValue {
+ const linkFieldValue = input as LinkFieldValue;
+ const result = '' in linkFieldValue;
+
+ return result;
+}
+
+/**
+ * typeguard for RichTextField
+ * @param {RichTextField | T} [input]
+ * @returns {boolean}
+ */
+export function isRichTextField(input: RichTextField | T): input is RichTextField {
+ const richtextfield = input as RichTextField;
+ const result =
+ 'value' in richtextfield &&
+ typeof richtextfield.value === 'string' &&
+ Object.keys(richtextfield).length >= 1 &&
+ Object.keys(richtextfield).length <= 2;
+
+ return result;
+}
+
+/**
+ * typeguard for TextField
+ * @param {TextField | T} [input]
+ * @returns {boolean}
+ */
+export function isTextField(input: TextField | T): input is TextField {
+ const textField = input as TextField;
+ const result =
+ 'value' in textField &&
+ Object.keys(textField).length >= 1 &&
+ Object.keys(textField).length <= 2;
+
+ return result;
+}
diff --git a/packages/sitecore-jss-nextjs-typeguards/test/setup.js b/packages/sitecore-jss-nextjs-typeguards/test/setup.js
new file mode 100644
index 0000000000..ba74f06621
--- /dev/null
+++ b/packages/sitecore-jss-nextjs-typeguards/test/setup.js
@@ -0,0 +1,5 @@
+require('ts-node/register/transpile-only');
+require('../src/tests/request.ts');
+require('../src/tests/shim.ts');
+require('../src/tests/jsdom-setup.ts');
+require('../src/tests/enzyme-setup.ts');
diff --git a/packages/sitecore-jss-nextjs-typeguards/tsconfig-esm.json b/packages/sitecore-jss-nextjs-typeguards/tsconfig-esm.json
new file mode 100644
index 0000000000..eb58bc1628
--- /dev/null
+++ b/packages/sitecore-jss-nextjs-typeguards/tsconfig-esm.json
@@ -0,0 +1,7 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "module": "es6",
+ "outDir": "dist/esm"
+ },
+}
diff --git a/packages/sitecore-jss-nextjs-typeguards/tsconfig.json b/packages/sitecore-jss-nextjs-typeguards/tsconfig.json
new file mode 100644
index 0000000000..9d2cf9eaff
--- /dev/null
+++ b/packages/sitecore-jss-nextjs-typeguards/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "target": "es6",
+ "module": "commonjs",
+ "jsx": "react",
+ "skipLibCheck": true,
+ "outDir": "dist/cjs",
+ "typeRoots": ["node_modules/@types"],
+ "declarationDir": "./types"
+ },
+ "exclude": [
+ "node_modules",
+ "types",
+ "typings",
+ "dist",
+ "context.d.ts",
+ "middleware.d.ts",
+ "editing.d.ts",
+ "monitoring.d.ts",
+ "site.d.ts",
+ "graphql.d.ts",
+ "utils.d.ts",
+ "src/tests/*",
+ "src/test-data/*",
+ "**/*.test.ts",
+ "**/*.test.tsx"
+ ]
+}
diff --git a/packages/sitecore-jss-nextjs/src/index.ts b/packages/sitecore-jss-nextjs/src/index.ts
index bff971f82c..1ca2981304 100644
--- a/packages/sitecore-jss-nextjs/src/index.ts
+++ b/packages/sitecore-jss-nextjs/src/index.ts
@@ -48,10 +48,16 @@ export {
PageViewInstance,
} from '@sitecore-jss/sitecore-jss/tracking';
export {
+ ContentTokenPhrases,
+ ContentTokenService,
DictionaryPhrases,
DictionaryService,
+ GraphQLContentTokenService,
+ GraphQLContentTokenServiceConfig,
GraphQLDictionaryService,
GraphQLDictionaryServiceConfig,
+ // TODO:RestContentTokenService,
+ // TODO:RestContentTokenServiceConfig,
RestDictionaryService,
RestDictionaryServiceConfig,
} from '@sitecore-jss/sitecore-jss/i18n';
diff --git a/packages/sitecore-jss/src/constants.ts b/packages/sitecore-jss/src/constants.ts
index 295c116dcf..08f5746481 100644
--- a/packages/sitecore-jss/src/constants.ts
+++ b/packages/sitecore-jss/src/constants.ts
@@ -4,6 +4,9 @@ export enum SitecoreTemplateId {
// /sitecore/templates/System/Dictionary/Dictionary entry
DictionaryEntry = '6d1cd89719364a3aa511289a94c2a7b1',
+
+ // /sitecore/templates/Feature/Experience Accelerator/Content Tokens/Content Token
+ ContentToken = '7d659ee9d4874d408a9210c6d68844c8',
}
export const FETCH_WITH = {
diff --git a/packages/sitecore-jss/src/debug.ts b/packages/sitecore-jss/src/debug.ts
index 692c342be8..da87446909 100644
--- a/packages/sitecore-jss/src/debug.ts
+++ b/packages/sitecore-jss/src/debug.ts
@@ -40,4 +40,5 @@ export default {
personalize: debug(`${rootNamespace}:personalize`),
errorpages: debug(`${rootNamespace}:errorpages`),
proxy: debug(`${rootNamespace}:proxy`),
+ contenttokens: debug(`${rootNamespace}:contenttokens`),
};
diff --git a/packages/sitecore-jss/src/i18n/content-token-service.ts b/packages/sitecore-jss/src/i18n/content-token-service.ts
new file mode 100644
index 0000000000..458091d37d
--- /dev/null
+++ b/packages/sitecore-jss/src/i18n/content-token-service.ts
@@ -0,0 +1,72 @@
+import { CacheClient, CacheOptions, MemoryCacheClient } from '../cache-client';
+
+/**
+ * Object model for Sitecore dictionary phrases
+ */
+export interface ContentTokenPhrases {
+ [k: string]: string;
+}
+
+/**
+ * Service that fetches dictionary data using Sitecore's GraphQL API.
+ */
+export interface ContentTokenService {
+ /**
+ * Fetch dictionary data for a language.
+ * @param {string} language the language to be used to fetch the dictionary
+ */
+ fetchContentTokens(language: string): Promise;
+}
+
+/**
+ * Base implementation of @see ContentTokenService that handles caching dictionary values
+ */
+export abstract class ContentTokenServiceBase
+ implements ContentTokenService, CacheClient {
+ private cache: CacheClient;
+
+ /**
+ * Initializes a new instance of @see ContentTokenService using the provided @see CacheOptions
+ * @param {CacheOptions} options Configuration options
+ */
+ constructor(public options: CacheOptions) {
+ this.cache = this.getCacheClient();
+ }
+
+ /**
+ * Caches a @see ContentTokenPhrases value for the specified cache key.
+ * @param {string} key The cache key.
+ * @param {ContentTokenPhrases} value The value to cache.
+ * @returns The value added to the cache.
+ * @mixes CacheClient
+ */
+ setCacheValue(key: string, value: ContentTokenPhrases): ContentTokenPhrases {
+ return this.cache.setCacheValue(key, value);
+ }
+
+ /**
+ * Retrieves a @see ContentTokenPhrases value from the cache.
+ * @param {string} key The cache key.
+ * @returns The @see ContentTokenPhrases value, or null if the specified key is not found in the cache.
+ */
+ getCacheValue(key: string): ContentTokenPhrases | null {
+ return this.cache.getCacheValue(key);
+ }
+
+ /**
+ * Gets a cache client that can cache data. Uses memory-cache as the default
+ * library for caching (@see MemoryCacheClient). Override this method if you
+ * want to use something else.
+ * @returns {CacheClient} implementation
+ */
+ protected getCacheClient(): CacheClient {
+ return new MemoryCacheClient(this.options);
+ }
+
+ /**
+ * Fetch dictionary data for a language.
+ * @param {string} language the language to be used to fetch the dictionary
+ * @returns {Promise}
+ */
+ abstract fetchContentTokens(language: string): Promise;
+}
diff --git a/packages/sitecore-jss/src/i18n/graphql-content-token-service.test.ts b/packages/sitecore-jss/src/i18n/graphql-content-token-service.test.ts
new file mode 100644
index 0000000000..33b9a32295
--- /dev/null
+++ b/packages/sitecore-jss/src/i18n/graphql-content-token-service.test.ts
@@ -0,0 +1,273 @@
+/* eslint-disable no-unused-expressions */
+import { expect } from 'chai';
+import sinon, { SinonSpy } from 'sinon';
+import nock from 'nock';
+import { SitecoreTemplateId } from '../constants';
+import { GraphQLClient, GraphQLRequestClient } from '../graphql-request-client';
+import { queryError, GraphQLContentTokenServiceConfig } from './graphql-content-token-service';
+import { GraphQLContentTokenService } from '.';
+import contentTokenQueryResponse from '../test-data/mockContentTokenQueryResponse.json';
+import appRootQueryResponse from '../test-data/mockAppRootQueryResponse.json';
+
+class TestService extends GraphQLContentTokenService {
+ public client: GraphQLClient;
+ constructor(options: GraphQLContentTokenServiceConfig) {
+ super(options);
+ this.client = this.getGraphQLClient();
+ }
+}
+
+describe('GraphQLContentTokenService', () => {
+ const endpoint = 'http://site';
+ const siteName = 'site-name';
+ const apiKey = 'api-key';
+ const rootItemId = '{GUID}';
+ const clientFactory = GraphQLRequestClient.createClientFactory({
+ endpoint,
+ apiKey,
+ });
+
+ afterEach(() => {
+ nock.cleanAll();
+ });
+
+ it('should fetch content token phrases using clientFactory', async () => {
+ nock(endpoint, { reqheaders: { sc_apikey: apiKey } })
+ .post('/', /ContentTokenSearch/gi)
+ .reply(200, contentTokenQueryResponse);
+
+ const service = new GraphQLContentTokenService({
+ siteName,
+ rootItemId,
+ cacheEnabled: false,
+ clientFactory,
+ });
+ const result = await service.fetchContentTokens('en');
+ expect(result.foo).to.equal('foo');
+ expect(result.bar).to.equal('bar');
+ });
+
+ it('should attempt to fetch the rootItemId, if rootItemId not provided', async () => {
+ nock(endpoint)
+ .post('/', /AppRootQuery/)
+ .reply(200, appRootQueryResponse);
+
+ nock(endpoint)
+ .post('/', (body) => body.variables.rootItemId === 'GUIDGUIDGUID')
+ .reply(200, contentTokenQueryResponse);
+
+ const service = new GraphQLContentTokenService({
+ clientFactory,
+ siteName,
+ cacheEnabled: false,
+ });
+ const result = await service.fetchContentTokens('en');
+ expect(result).to.have.all.keys('foo', 'bar');
+ // eslint-disable-next-line no-unused-expressions
+ expect(nock.isDone()).to.be.true;
+ });
+
+ it('should use a custom rootItemId, if provided', async () => {
+ const customRootId = 'cats';
+
+ nock(endpoint)
+ .post('/', (body) => body.variables.rootItemId === customRootId)
+ .reply(200, contentTokenQueryResponse);
+
+ const service = new GraphQLContentTokenService({
+ clientFactory,
+ siteName,
+ cacheEnabled: false,
+ rootItemId: customRootId,
+ });
+ const result = await service.fetchContentTokens('en');
+ expect(result).to.have.all.keys('foo', 'bar');
+ });
+
+ it('should use a jssTemplateId, if provided', async () => {
+ const jssAppTemplateId = '{71d608ca-ac9c-4f1c-8e0a-85a6946e30f8}';
+ const randomId = '{412286b7-6d4f-4deb-80e9-108ee986c6e9}';
+
+ nock(endpoint)
+ .post('/', (body) => body.variables.jssAppTemplateId === jssAppTemplateId)
+ .reply(200, {
+ data: {
+ layout: {
+ homePage: {
+ rootItem: [
+ {
+ id: randomId,
+ },
+ ],
+ },
+ },
+ },
+ });
+
+ nock(endpoint)
+ .post('/', (body) => body.variables.rootItemId === randomId)
+ .reply(200, contentTokenQueryResponse);
+
+ const service = new GraphQLContentTokenService({
+ clientFactory,
+ siteName,
+ cacheEnabled: false,
+ jssAppTemplateId,
+ });
+
+ const result = await service.fetchContentTokens('en');
+ expect(result).to.have.all.keys('foo', 'bar');
+ });
+
+ it('should throw error if could not resolve rootItemId', async () => {
+ nock(endpoint)
+ .post('/', /AppRootQuery/)
+ .reply(200, {
+ data: {
+ layout: {
+ homePage: null,
+ },
+ },
+ });
+
+ const service = new GraphQLContentTokenService({
+ clientFactory,
+ siteName,
+ cacheEnabled: false,
+ });
+
+ await service.fetchContentTokens('en').catch((error) => {
+ expect(error).to.be.instanceOf(Error);
+ expect(error.message).to.equal(queryError);
+ });
+ });
+
+ it('should use default pageSize, if pageSize not provided', async () => {
+ nock(endpoint)
+ .post(
+ '/',
+ (body) =>
+ body.query.indexOf('$pageSize: Int = 10') > 0 && body.variables.pageSize === undefined
+ )
+ .reply(200, contentTokenQueryResponse);
+
+ const service = new GraphQLContentTokenService({
+ clientFactory,
+ siteName,
+ rootItemId,
+ cacheEnabled: false,
+ pageSize: undefined,
+ });
+ const result = await service.fetchContentTokens('en');
+ expect(result).to.have.all.keys('foo', 'bar');
+ });
+
+ it('should use a custom pageSize, if provided', async () => {
+ const customPageSize = 2;
+
+ nock(endpoint)
+ .post('/', (body) => body.variables.pageSize === customPageSize)
+ .reply(200, contentTokenQueryResponse);
+
+ const service = new GraphQLContentTokenService({
+ clientFactory,
+ siteName,
+ rootItemId,
+ cacheEnabled: false,
+ pageSize: customPageSize,
+ });
+ const result = await service.fetchContentTokens('en');
+ expect(result).to.have.all.keys('foo', 'bar');
+ });
+
+ it('should use custom content token template ID, if provided', async () => {
+ const customTemplateId = 'custom-template-id';
+
+ nock(endpoint)
+ .post('/', (body) => body.variables.templates === customTemplateId)
+ .reply(200, contentTokenQueryResponse);
+
+ const service = new GraphQLContentTokenService({
+ clientFactory,
+ siteName,
+ rootItemId,
+ cacheEnabled: false,
+ contentTokenTemplateId: customTemplateId,
+ });
+ const result = await service.fetchContentTokens('en');
+ expect(result).to.have.all.keys('foo', 'bar');
+ });
+
+ it('should use default content token template ID, if template ID not provided', async () => {
+ nock(endpoint)
+ .post('/', (body) => body.variables.templates === SitecoreTemplateId.ContentToken)
+ .reply(200, contentTokenQueryResponse);
+
+ const service = new GraphQLContentTokenService({
+ clientFactory,
+ siteName,
+ rootItemId,
+ cacheEnabled: false,
+ });
+ const result = await service.fetchContentTokens('en');
+ expect(result).to.have.all.keys('foo', 'bar');
+ });
+
+ it('should use cache', async () => {
+ nock(endpoint, { reqheaders: { sc_apikey: apiKey } })
+ .post('/', /ContentTokenSearch/gi)
+ .reply(200, contentTokenQueryResponse);
+
+ const service = new GraphQLContentTokenService({
+ clientFactory,
+ siteName,
+ rootItemId,
+ cacheEnabled: true,
+ cacheTimeout: 2,
+ });
+
+ const result1 = await service.fetchContentTokens('en');
+ expect(result1).to.have.all.keys('foo', 'bar');
+
+ const result2 = await service.fetchContentTokens('en');
+ expect(result2).to.have.all.keys('foo', 'bar');
+ });
+
+ it('should provide a default GraphQL client', () => {
+ const service = new TestService({
+ clientFactory,
+ siteName,
+ rootItemId,
+ cacheEnabled: false,
+ });
+
+ const graphQLClient = service.client as GraphQLClient;
+ const graphQLRequestClient = service.client as GraphQLRequestClient;
+ // eslint-disable-next-line no-unused-expressions
+ expect(graphQLClient).to.exist;
+ // eslint-disable-next-line no-unused-expressions
+ expect(graphQLRequestClient).to.exist;
+ });
+
+ it('should call clientFactory with the correct arguments', () => {
+ const clientFactorySpy: SinonSpy = sinon.spy();
+ const mockServiceConfig = {
+ siteName: 'supersite',
+ clientFactory: clientFactorySpy,
+ retries: 3,
+ retryStrategy: {
+ getDelay: () => 1000,
+ shouldRetry: () => true,
+ },
+ };
+
+ new GraphQLContentTokenService(mockServiceConfig);
+
+ expect(clientFactorySpy.calledOnce).to.be.true;
+
+ const calledWithArgs = clientFactorySpy.firstCall.args[0];
+ expect(calledWithArgs.debugger).to.exist;
+ expect(calledWithArgs.retries).to.equal(mockServiceConfig.retries);
+ expect(calledWithArgs.retryStrategy).to.deep.equal(mockServiceConfig.retryStrategy);
+ });
+});
diff --git a/packages/sitecore-jss/src/i18n/graphql-content-token-service.ts b/packages/sitecore-jss/src/i18n/graphql-content-token-service.ts
new file mode 100644
index 0000000000..bfd683a99a
--- /dev/null
+++ b/packages/sitecore-jss/src/i18n/graphql-content-token-service.ts
@@ -0,0 +1,200 @@
+import {
+ GraphQLClient,
+ GraphQLRequestClientConfig,
+ GraphQLRequestClientFactory,
+} from '../graphql-request-client';
+import { SitecoreTemplateId } from '../constants';
+import { ContentTokenPhrases, ContentTokenServiceBase } from './content-token-service';
+import { CacheOptions } from '../cache-client';
+import { getAppRootId, SearchQueryResult, SearchQueryVariables } from '../graphql';
+import debug from '../debug';
+
+/** @private */
+export const queryError =
+ 'Valid value for rootItemId not provided and failed to auto-resolve app root item.';
+
+const query = /* GraphQL */ `
+ query ContentTokenSearch(
+ $rootItemId: String!
+ $language: String!
+ $templates: String!
+ $pageSize: Int = 10
+ $after: String
+ ) {
+ search(
+ where: {
+ AND: [
+ { name: "_path", value: $rootItemId, operator: CONTAINS }
+ { name: "_language", value: $language }
+ { name: "_templates", value: $templates, operator: CONTAINS }
+ ]
+ }
+ first: $pageSize
+ after: $after
+ ) {
+ total
+ pageInfo {
+ endCursor
+ hasNext
+ }
+ results {
+ key: field(name: "Key") {
+ value
+ }
+ value: field(name: "Value") {
+ value
+ }
+ }
+ }
+ }
+`;
+
+/**
+ * Configuration options for @see GraphQLContentTokenService instances
+ */
+export interface GraphQLContentTokenServiceConfig
+ extends Omit,
+ CacheOptions,
+ Pick {
+ /**
+ * The name of the current Sitecore site. This is used to to determine the search query root
+ * in cases where one is not specified by the caller.
+ */
+ siteName: string;
+
+ /**
+ * A GraphQL Request Client Factory is a function that accepts configuration and returns an instance of a GraphQLRequestClient.
+ * This factory function is used to create and configure GraphQL clients for making GraphQL API requests.
+ */
+ clientFactory: GraphQLRequestClientFactory;
+
+ /**
+ * Optional. The template ID to use when searching for content token entries.
+ * @default '7d659ee9d4874d408a9210c6d68844c8' (/sitecore/templates/Feature/Experience Accelerator/Content Tokens/Content Token)
+ */
+ contentTokenTemplateId?: string;
+
+ /**
+ * Optional. The template ID of a JSS App to use when searching for the appRootId.
+ * @default '061cba1554744b918a0617903b102b82' (/sitecore/templates/Foundation/JavaScript Services/App)
+ */
+ jssAppTemplateId?: string;
+}
+
+/**
+ * The schema of data returned in response to a content token query request.
+ */
+export type ContentTokenQueryResult = {
+ key: { value: string };
+ value: { value: string };
+};
+
+/**
+ * Service that fetch content token data using Sitecore's GraphQL API.
+ * @augments ContentTokenServiceBase
+ */
+export class GraphQLContentTokenService extends ContentTokenServiceBase {
+ private graphQLClient: GraphQLClient;
+
+ /**
+ * Creates an instance of graphQL content token service with the provided options
+ * @param {GraphQLContentTokenService} options instance
+ */
+ constructor(public options: GraphQLContentTokenServiceConfig) {
+ super(options);
+ this.graphQLClient = this.getGraphQLClient();
+ }
+
+ /**
+ * Fetches content token data for internalization. Uses search query by default
+ * @param {string} language the language to fetch
+ * @returns {Promise} content token phrases
+ * @throws {Error} if the app root was not found for the specified site and language.
+ */
+ async fetchContentTokens(language: string): Promise {
+ const cacheKey = this.options.siteName + language;
+ const cachedValue = this.getCacheValue(cacheKey);
+ if (cachedValue) {
+ debug.contenttokens(
+ 'using cached content token data for %s %s',
+ language,
+ this.options.siteName
+ );
+ return cachedValue;
+ }
+
+ const phrases = await this.fetchWithSearchQuery(language);
+
+ this.setCacheValue(cacheKey, phrases);
+ return phrases;
+ }
+
+ /**
+ * Fetches content token data with search query
+ * This is the default behavior for non-XMCloud deployments
+ * @param {string} language the language to fetch
+ * @default query (@see query)
+ * @returns {Promise} content token phrases
+ * @throws {Error} if the app root was not found for the specified site and language.
+ */
+ async fetchWithSearchQuery(language: string): Promise {
+ debug.contenttokens('fetching site root for %s %s', language, this.options.siteName);
+
+ // If the caller does not specify a root item ID, then we try to figure it out
+ const rootItemId =
+ this.options.rootItemId ||
+ (await getAppRootId(
+ this.graphQLClient,
+ this.options.siteName,
+ language,
+ this.options.jssAppTemplateId
+ ));
+
+ if (!rootItemId) {
+ throw new Error(queryError);
+ }
+
+ debug.contenttokens('fetching content token data for %s %s', language, this.options.siteName);
+ const phrases: ContentTokenPhrases = {};
+ let results: ContentTokenQueryResult[] = [];
+ let hasNext = true;
+ let after = '';
+
+ while (hasNext) {
+ const fetchResponse = await this.graphQLClient.request<
+ SearchQueryResult
+ >(query, {
+ rootItemId,
+ language,
+ templates: this.options.contentTokenTemplateId || SitecoreTemplateId.ContentToken,
+ pageSize: this.options.pageSize,
+ after,
+ });
+
+ results = results.concat(fetchResponse?.search?.results);
+ hasNext = fetchResponse.search.pageInfo.hasNext;
+ after = fetchResponse.search.pageInfo.endCursor;
+ }
+
+ results.forEach((item) => (phrases[item.key.value] = item.value.value));
+
+ return phrases;
+ }
+
+ /**
+ * Gets a GraphQL client that can make requests to the API. Uses graphql-request as the default
+ * library for fetching graphql data (@see GraphQLRequestClient). Override this method if you
+ * want to use something else.
+ * @returns {GraphQLClient} implementation
+ */
+ protected getGraphQLClient(): GraphQLClient {
+ if (!this.options.clientFactory) {
+ throw new Error('clientFactory needs to be provided when initializing GraphQL client.');
+ }
+ return this.options.clientFactory({
+ debugger: debug.contenttokens,
+ retries: this.options.retries,
+ retryStrategy: this.options.retryStrategy,
+ });
+ }
+}
diff --git a/packages/sitecore-jss/src/i18n/index.ts b/packages/sitecore-jss/src/i18n/index.ts
index 2d6f9242cf..5534199182 100644
--- a/packages/sitecore-jss/src/i18n/index.ts
+++ b/packages/sitecore-jss/src/i18n/index.ts
@@ -1,3 +1,12 @@
+export {
+ ContentTokenPhrases,
+ ContentTokenService,
+ ContentTokenServiceBase,
+} from './content-token-service';
+export {
+ GraphQLContentTokenServiceConfig,
+ GraphQLContentTokenService,
+} from './graphql-content-token-service';
export { DictionaryPhrases, DictionaryService, DictionaryServiceBase } from './dictionary-service';
export {
GraphQLDictionaryServiceConfig,
diff --git a/packages/sitecore-jss/src/test-data/mockContentTokenQueryResponse.json b/packages/sitecore-jss/src/test-data/mockContentTokenQueryResponse.json
new file mode 100644
index 0000000000..7c1bbf9fef
--- /dev/null
+++ b/packages/sitecore-jss/src/test-data/mockContentTokenQueryResponse.json
@@ -0,0 +1,29 @@
+{
+ "data": {
+ "search": {
+ "total": 5,
+ "pageInfo": {
+ "endCursor": "NQ==",
+ "hasNext": false
+ },
+ "results": [
+ {
+ "key": {
+ "value": "foo"
+ },
+ "value": {
+ "value": "foo"
+ }
+ },
+ {
+ "key": {
+ "value": "bar"
+ },
+ "value": {
+ "value": "bar"
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/ref-docs/sitecore-jss/classes/i18n.ContentTokenServiceBase.md b/ref-docs/sitecore-jss/classes/i18n.ContentTokenServiceBase.md
new file mode 100644
index 0000000000..9b6d95a9f3
--- /dev/null
+++ b/ref-docs/sitecore-jss/classes/i18n.ContentTokenServiceBase.md
@@ -0,0 +1,201 @@
+[@sitecore-jss/sitecore-jss](../README.md) / [i18n](../modules/i18n.md) / ContentTokenServiceBase
+
+# Class: ContentTokenServiceBase
+
+[i18n](../modules/i18n.md).ContentTokenServiceBase
+
+Base implementation of
+
+**`See`**
+
+ContentTokenService that handles caching content token values
+
+## Hierarchy
+
+- **`ContentTokenServiceBase`**
+
+ ↳ [`GraphQLContentTokenService`](i18n.GraphQLContentTokenService.md)
+
+## Implements
+
+- [`ContentTokenService`](../interfaces/i18n.ContentTokenService.md)
+- `CacheClient`\<[`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md)\>
+
+## Table of contents
+
+### Constructors
+
+- [constructor](i18n.ContentTokenServiceBase.md#constructor)
+
+### Properties
+
+- [cache](i18n.ContentTokenServiceBase.md#cache)
+- [options](i18n.ContentTokenServiceBase.md#options)
+
+### Methods
+
+- [fetchContentTokens](i18n.ContentTokenServiceBase.md#fetchContentTokens)
+- [getCacheClient](i18n.ContentTokenServiceBase.md#getcacheclient)
+- [getCacheValue](i18n.ContentTokenServiceBase.md#getcachevalue)
+- [setCacheValue](i18n.ContentTokenServiceBase.md#setcachevalue)
+
+## Constructors
+
+### constructor
+
+• **new ContentTokenServiceBase**(`options`)
+
+Initializes a new instance of
+
+#### Parameters
+
+| Name | Type | Description |
+| :------ | :------ | :------ |
+| `options` | `CacheOptions` | Configuration options |
+
+**`See`**
+
+ - ContentTokenService using the provided
+ - CacheOptions
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/content-token-service.ts:32](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/content-token-service.ts#L32)
+
+## Properties
+
+### cache
+
+• `Private` **cache**: `CacheClient`\<[`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md)\>
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/content-token-service.ts:26](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/content-token-service.ts#L26)
+
+___
+
+### options
+
+• **options**: `CacheOptions`
+
+Configuration options
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/content-token-service.ts:32](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/content-token-service.ts#L32)
+
+## Methods
+
+### fetchContentTokens
+
+â–¸ `Abstract` **fetchContentTokens**(`language`): `Promise`\<[`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md)\>
+
+Fetch content tokens data for a language.
+
+#### Parameters
+
+| Name | Type | Description |
+| :------ | :------ | :------ |
+| `language` | `string` | the language to be used to fetch the content tokens |
+
+#### Returns
+
+`Promise`\<[`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md)\>
+
+#### Implementation of
+
+[ContentTokenService](../interfaces/i18n.ContentTokenService.md).[fetchContentTokens](../interfaces/i18n.ContentTokenService.md#fetchContentTokens)
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/content-token-service.ts:71](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/content-token-service.ts#L71)
+
+___
+
+### getCacheClient
+
+â–¸ `Protected` **getCacheClient**(): `CacheClient`\<[`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md)\>
+
+Gets a cache client that can cache data. Uses memory-cache as the default
+library for caching (@see MemoryCacheClient). Override this method if you
+want to use something else.
+
+#### Returns
+
+`CacheClient`\<[`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md)\>
+
+implementation
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/content-token-service.ts:62](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/content-token-service.ts#L62)
+
+___
+
+### getCacheValue
+
+â–¸ **getCacheValue**(`key`): ``null`` \| [`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md)
+
+Retrieves a
+
+#### Parameters
+
+| Name | Type | Description |
+| :------ | :------ | :------ |
+| `key` | `string` | The cache key. |
+
+#### Returns
+
+``null`` \| [`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md)
+
+The
+
+**`See`**
+
+ - ContentTokenPhrases value from the cache.
+ - ContentTokenPhrases value, or null if the specified key is not found in the cache.
+
+#### Implementation of
+
+CacheClient.getCacheValue
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/content-token-service.ts:52](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/content-token-service.ts#L52)
+
+___
+
+### setCacheValue
+
+â–¸ **setCacheValue**(`key`, `value`): [`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md)
+
+Caches a
+
+#### Parameters
+
+| Name | Type | Description |
+| :------ | :------ | :------ |
+| `key` | `string` | The cache key. |
+| `value` | [`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md) | The value to cache. |
+
+#### Returns
+
+[`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md)
+
+The value added to the cache.
+
+**`See`**
+
+ContentTokenPhrases value for the specified cache key.
+
+**`Mixes`**
+
+CacheClient
+
+#### Implementation of
+
+CacheClient.setCacheValue
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/content-token-service.ts:43](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/content-token-service.ts#L43)
diff --git a/ref-docs/sitecore-jss/classes/i18n.GraphQLContentTokenService.md b/ref-docs/sitecore-jss/classes/i18n.GraphQLContentTokenService.md
new file mode 100644
index 0000000000..f0a1d43e6e
--- /dev/null
+++ b/ref-docs/sitecore-jss/classes/i18n.GraphQLContentTokenService.md
@@ -0,0 +1,262 @@
+[@sitecore-jss/sitecore-jss](../README.md) / [i18n](../modules/i18n.md) / GraphQLContentTokenService
+
+# Class: GraphQLContentTokenService
+
+[i18n](../modules/i18n.md).GraphQLContentTokenService
+
+Service that fetch content token data using Sitecore's GraphQL API.
+
+## Hierarchy
+
+- [`ContentTokenServiceBase`](i18n.ContentTokenServiceBase.md)
+
+ ↳ **`GraphQLContentTokenService`**
+
+## Table of contents
+
+### Constructors
+
+- [constructor](i18n.GraphQLContentTokenService.md#constructor)
+
+### Properties
+
+- [graphQLClient](i18n.GraphQLContentTokenService.md#graphqlclient)
+- [options](i18n.GraphQLContentTokenService.md#options)
+
+### Methods
+
+- [fetchContentTokens](i18n.GraphQLContentTokenService.md#fetchContentTokens)
+- [fetchWithSearchQuery](i18n.GraphQLContentTokenService.md#fetchwithsearchquery)
+- [getCacheClient](i18n.GraphQLContentTokenService.md#getcacheclient)
+- [getCacheValue](i18n.GraphQLContentTokenService.md#getcachevalue)
+- [getGraphQLClient](i18n.GraphQLContentTokenService.md#getgraphqlclient)
+- [setCacheValue](i18n.GraphQLContentTokenService.md#setcachevalue)
+
+## Constructors
+
+### constructor
+
+• **new GraphQLContentTokenService**(`options`)
+
+Creates an instance of graphQL content token service with the provided options
+
+#### Parameters
+
+| Name | Type | Description |
+| :------ | :------ | :------ |
+| `options` | [`GraphQLContentTokenServiceConfig`](../interfaces/i18n.GraphQLContentTokenServiceConfig.md) | instance |
+
+#### Overrides
+
+[ContentTokenServiceBase](i18n.ContentTokenServiceBase.md).[constructor](i18n.ContentTokenServiceBase.md#constructor)
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/graphql-content-token-service.ts:103](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/graphql-content-token-service.ts#L103)
+
+## Properties
+
+### graphQLClient
+
+• `Private` **graphQLClient**: [`GraphQLClient`](../interfaces/index.GraphQLClient.md)
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/graphql-content-token-service.ts:97](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/graphql-content-token-service.ts#L97)
+
+___
+
+### options
+
+• **options**: [`GraphQLContentTokenServiceConfig`](../interfaces/i18n.GraphQLContentTokenServiceConfig.md)
+
+instance
+
+#### Inherited from
+
+[ContentTokenServiceBase](i18n.ContentTokenServiceBase.md).[options](i18n.ContentTokenServiceBase.md#options)
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/graphql-content-token-service.ts:103](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/graphql-content-token-service.ts#L103)
+
+## Methods
+
+### fetchContentTokens
+
+â–¸ **fetchContentTokens**(`language`): `Promise`\<[`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md)\>
+
+Fetches content token data for internalization. Uses search query by default
+
+#### Parameters
+
+| Name | Type | Description |
+| :------ | :------ | :------ |
+| `language` | `string` | the language to fetch |
+
+#### Returns
+
+`Promise`\<[`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md)\>
+
+content token phrases
+
+**`Throws`**
+
+if the app root was not found for the specified site and language.
+
+#### Overrides
+
+[ContentTokenServiceBase](i18n.ContentTokenServiceBase.md).[fetchContentTokens](i18n.ContentTokenServiceBase.md#fetchContentTokens)
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/graphql-content-token-service.ts:114](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/graphql-content-token-service.ts#L114)
+
+___
+
+### fetchWithSearchQuery
+
+â–¸ **fetchWithSearchQuery**(`language`): `Promise`\<[`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md)\>
+
+Fetches content token data with search query
+This is the default behavior for non-XMCloud deployments
+
+#### Parameters
+
+| Name | Type | Description |
+| :------ | :------ | :------ |
+| `language` | `string` | the language to fetch |
+
+#### Returns
+
+`Promise`\<[`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md)\>
+
+content token phrases
+
+**`Default`**
+
+```ts
+query (@see query)
+```
+
+**`Throws`**
+
+if the app root was not found for the specified site and language.
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/graphql-content-token-service.ts:136](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/graphql-content-token-service.ts#L136)
+
+___
+
+### getCacheClient
+
+â–¸ `Protected` **getCacheClient**(): `CacheClient`\<[`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md)\>
+
+Gets a cache client that can cache data. Uses memory-cache as the default
+library for caching (@see MemoryCacheClient). Override this method if you
+want to use something else.
+
+#### Returns
+
+`CacheClient`\<[`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md)\>
+
+implementation
+
+#### Inherited from
+
+[ContentTokenServiceBase](i18n.ContentTokenServiceBase.md).[getCacheClient](i18n.ContentTokenServiceBase.md#getcacheclient)
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/content-token-service.ts:62](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/content-token-service.ts#L62)
+
+___
+
+### getCacheValue
+
+â–¸ **getCacheValue**(`key`): ``null`` \| [`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md)
+
+Retrieves a
+
+#### Parameters
+
+| Name | Type | Description |
+| :------ | :------ | :------ |
+| `key` | `string` | The cache key. |
+
+#### Returns
+
+``null`` \| [`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md)
+
+The
+
+**`See`**
+
+ - ContentTokenPhrases value from the cache.
+ - ContentTokenPhrases value, or null if the specified key is not found in the cache.
+
+#### Inherited from
+
+[ContentTokenServiceBase](i18n.ContentTokenServiceBase.md).[getCacheValue](i18n.ContentTokenServiceBase.md#getcachevalue)
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/content-token-service.ts:52](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/content-token-service.ts#L52)
+
+___
+
+### getGraphQLClient
+
+â–¸ `Protected` **getGraphQLClient**(): [`GraphQLClient`](../interfaces/index.GraphQLClient.md)
+
+Gets a GraphQL client that can make requests to the API. Uses graphql-request as the default
+library for fetching graphql data (@see GraphQLRequestClient). Override this method if you
+want to use something else.
+
+#### Returns
+
+[`GraphQLClient`](../interfaces/index.GraphQLClient.md)
+
+implementation
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/graphql-content-token-service.ts:185](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/graphql-content-token-service.ts#L185)
+
+___
+
+### setCacheValue
+
+â–¸ **setCacheValue**(`key`, `value`): [`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md)
+
+Caches a
+
+#### Parameters
+
+| Name | Type | Description |
+| :------ | :------ | :------ |
+| `key` | `string` | The cache key. |
+| `value` | [`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md) | The value to cache. |
+
+#### Returns
+
+[`ContentTokenPhrases`](../interfaces/i18n.ContentTokenPhrases.md)
+
+The value added to the cache.
+
+**`See`**
+
+ContentTokenPhrases value for the specified cache key.
+
+**`Mixes`**
+
+CacheClient
+
+#### Inherited from
+
+[ContentTokenServiceBase](i18n.ContentTokenServiceBase.md).[setCacheValue](i18n.ContentTokenServiceBase.md#setcachevalue)
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/content-token-service.ts:43](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/content-token-service.ts#L43)
diff --git a/ref-docs/sitecore-jss/interfaces/i18n.ContentTokenPhrases.md b/ref-docs/sitecore-jss/interfaces/i18n.ContentTokenPhrases.md
new file mode 100644
index 0000000000..7f9b87bf8d
--- /dev/null
+++ b/ref-docs/sitecore-jss/interfaces/i18n.ContentTokenPhrases.md
@@ -0,0 +1,11 @@
+[@sitecore-jss/sitecore-jss](../README.md) / [i18n](../modules/i18n.md) / ContentTokenPhrases
+
+# Interface: ContentTokenPhrases
+
+[i18n](../modules/i18n.md).ContentTokenPhrases
+
+Object model for Sitecore content token phrases
+
+## Indexable
+
+â–ª [k: `string`]: `string`
\ No newline at end of file
diff --git a/ref-docs/sitecore-jss/interfaces/i18n.ContentTokenService.md b/ref-docs/sitecore-jss/interfaces/i18n.ContentTokenService.md
new file mode 100644
index 0000000000..01d24e91f6
--- /dev/null
+++ b/ref-docs/sitecore-jss/interfaces/i18n.ContentTokenService.md
@@ -0,0 +1,39 @@
+[@sitecore-jss/sitecore-jss](../README.md) / [i18n](../modules/i18n.md) / ContentTokenService
+
+# Interface: ContentTokenService
+
+[i18n](../modules/i18n.md).ContentTokenService
+
+Service that fetches content token data using Sitecore's GraphQL API.
+
+## Implemented by
+
+- [`ContentTokenServiceBase`](../classes/i18n.ContentTokenServiceBase.md)
+
+## Table of contents
+
+### Methods
+
+- [fetchContentTokens](i18n.ContentTokenService.md#fetchContentTokens)
+
+## Methods
+
+### fetchContentTokens
+
+â–¸ **fetchContentTokens**(`language`): `Promise`\<[`ContentTokenPhrases`](i18n.ContentTokenPhrases.md)\>
+
+Fetch content token data for a language.
+
+#### Parameters
+
+| Name | Type | Description |
+| :------ | :------ | :------ |
+| `language` | `string` | the language to be used to fetch the content token |
+
+#### Returns
+
+`Promise`\<[`ContentTokenPhrases`](i18n.ContentTokenPhrases.md)\>
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/content-token-service.ts:18](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/content-token-service.ts#L18)
diff --git a/ref-docs/sitecore-jss/interfaces/i18n.GraphQLContentTokenServiceConfig.md b/ref-docs/sitecore-jss/interfaces/i18n.GraphQLContentTokenServiceConfig.md
new file mode 100644
index 0000000000..2649d19340
--- /dev/null
+++ b/ref-docs/sitecore-jss/interfaces/i18n.GraphQLContentTokenServiceConfig.md
@@ -0,0 +1,218 @@
+[@sitecore-jss/sitecore-jss](../README.md) / [i18n](../modules/i18n.md) / GraphQLContentTokenServiceConfig
+
+# Interface: GraphQLContentTokenServiceConfig
+
+[i18n](../modules/i18n.md).GraphQLContentTokenServiceConfig
+
+Configuration options for
+
+**`See`**
+
+GraphQLContentTokenService instances
+
+## Hierarchy
+
+- `Omit`\<[`SearchQueryVariables`](graphql.SearchQueryVariables.md), ``"language"``\>
+
+- `CacheOptions`
+
+- `Pick`\<[`GraphQLRequestClientConfig`](../modules/index.md#graphqlrequestclientconfig), ``"retries"`` \| ``"retryStrategy"``\>
+
+ ↳ **`GraphQLContentTokenServiceConfig`**
+
+## Table of contents
+
+### Properties
+
+- [cacheEnabled](i18n.GraphQLContentTokenServiceConfig.md#cacheenabled)
+- [cacheTimeout](i18n.GraphQLContentTokenServiceConfig.md#cachetimeout)
+- [clientFactory](i18n.GraphQLContentTokenServiceConfig.md#clientfactory)
+- [contentTokenTemplateId](i18n.GraphQLContentTokenServiceConfig.md#contentTokenTemplateId)
+- [jssAppTemplateId](i18n.GraphQLContentTokenServiceConfig.md#jssapptemplateid)
+- [pageSize](i18n.GraphQLContentTokenServiceConfig.md#pagesize)
+- [retries](i18n.GraphQLContentTokenServiceConfig.md#retries)
+- [retryStrategy](i18n.GraphQLContentTokenServiceConfig.md#retrystrategy)
+- [rootItemId](i18n.GraphQLContentTokenServiceConfig.md#rootitemid)
+- [siteName](i18n.GraphQLContentTokenServiceConfig.md#sitename)
+- [templates](i18n.GraphQLContentTokenServiceConfig.md#templates)
+
+## Properties
+
+### cacheEnabled
+
+• `Optional` **cacheEnabled**: `boolean`
+
+Enable/disable caching mechanism
+
+**`Default`**
+
+```ts
+true
+```
+
+#### Inherited from
+
+CacheOptions.cacheEnabled
+
+#### Defined in
+
+[packages/sitecore-jss/src/cache-client.ts:40](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/cache-client.ts#L40)
+
+___
+
+### cacheTimeout
+
+• `Optional` **cacheTimeout**: `number`
+
+Cache timeout (sec)
+
+**`Default`**
+
+```ts
+60
+```
+
+#### Inherited from
+
+CacheOptions.cacheTimeout
+
+#### Defined in
+
+[packages/sitecore-jss/src/cache-client.ts:45](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/cache-client.ts#L45)
+
+___
+
+### clientFactory
+
+• **clientFactory**: [`GraphQLRequestClientFactory`](../modules/index.md#graphqlrequestclientfactory)
+
+A GraphQL Request Client Factory is a function that accepts configuration and returns an instance of a GraphQLRequestClient.
+This factory function is used to create and configure GraphQL clients for making GraphQL API requests.
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/graphql-content-token-service.ts:94](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/graphql-content-token-service.ts#L94)
+
+___
+
+### contentTokenTemplateId
+
+• `Optional` **contentTokenTemplateId**: `string`
+
+Optional. The template ID to use when searching for dictionary entries.
+
+**`Default`**
+
+```ts
+'7d659ee9d4874d408a9210c6d68844c8' (/sitecore/templates/Feature/Experience Accelerator/Content Tokens/Content Token)
+```
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/graphql-content-token-service.ts:75](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/graphql-content-token-service.ts#L75)
+
+___
+
+### jssAppTemplateId
+
+• `Optional` **jssAppTemplateId**: `string`
+
+Optional. The template ID of a JSS App to use when searching for the appRootId.
+
+**`Default`**
+
+```ts
+'061cba1554744b918a0617903b102b82' (/sitecore/templates/Foundation/JavaScript Services/App)
+```
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/graphql-content-token-service.ts:81](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/graphql-content-token-service.ts#L81)
+
+___
+
+### pageSize
+
+• `Optional` **pageSize**: `number`
+
+common variable for all GraphQL queries
+it will be used for every type of query to regulate result batch size
+Optional. How many result items to fetch in each GraphQL call. This is needed for pagination.
+
+**`Default`**
+
+```ts
+10
+```
+
+#### Inherited from
+
+Omit.pageSize
+___
+
+### retries
+
+• `Optional` **retries**: `number`
+
+Number of retries for client. Will use the specified `retryStrategy`.
+
+#### Inherited from
+
+Pick.retries
+
+#### Defined in
+
+[packages/sitecore-jss/src/graphql-request-client.ts:83](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/graphql-request-client.ts#L83)
+
+___
+
+### retryStrategy
+
+• `Optional` **retryStrategy**: [`RetryStrategy`](index.RetryStrategy.md)
+
+Retry strategy for the client. Uses `DefaultRetryStrategy` by default with exponential
+back-off factor of 2 for codes 429, 502, 503, 504, 520, 521, 522, 523, 524.
+
+#### Inherited from
+
+Pick.retryStrategy
+
+#### Defined in
+
+[packages/sitecore-jss/src/graphql-request-client.ts:88](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/graphql-request-client.ts#L88)
+
+___
+
+### rootItemId
+
+• `Optional` **rootItemId**: `string`
+
+Optional. The ID of the search root item. Fetch items that have this item as an ancestor.
+
+#### Inherited from
+
+Omit.rootItemId
+___
+
+### siteName
+
+• **siteName**: `string`
+
+The name of the current Sitecore site. This is used to to determine the search query root
+in cases where one is not specified by the caller.
+
+#### Defined in
+
+[packages/sitecore-jss/src/i18n/graphql-content-token-service.ts:63](https://github.com/Sitecore/jss/blob/2794c8c94/packages/sitecore-jss/src/i18n/graphql-content-token-service.ts#L63)
+
+___
+
+### templates
+
+• `Optional` **templates**: `string`
+
+Optional. Sitecore template ID(s). Fetch items that inherit from this template(s).
+
+#### Inherited from
+
+Omit.templates
diff --git a/ref-docs/sitecore-jss/modules/i18n.md b/ref-docs/sitecore-jss/modules/i18n.md
index 77d9f8dc9a..7d846c3ce0 100644
--- a/ref-docs/sitecore-jss/modules/i18n.md
+++ b/ref-docs/sitecore-jss/modules/i18n.md
@@ -6,14 +6,19 @@
### Classes
+- [ContentTokenServiceBase](../classes/i18n.ContentTokenServiceBase.md)
- [DictionaryServiceBase](../classes/i18n.DictionaryServiceBase.md)
+- [GraphQLContentTokenService](../classes/i18n.GraphQLContentTokenService.md)
- [GraphQLDictionaryService](../classes/i18n.GraphQLDictionaryService.md)
- [RestDictionaryService](../classes/i18n.RestDictionaryService.md)
### Interfaces
+- [ContentTokenPhrases](../interfaces/i18n.ContentTokenPhrases.md)
+- [ContentTokenService](../interfaces/i18n.ContentTokenService.md)
- [DictionaryPhrases](../interfaces/i18n.DictionaryPhrases.md)
- [DictionaryService](../interfaces/i18n.DictionaryService.md)
+- [GraphQLContentTokenServiceConfig](../interfaces/i18n.GraphQLContentTokenServiceConfig.md)
- [GraphQLDictionaryServiceConfig](../interfaces/i18n.GraphQLDictionaryServiceConfig.md)
### Type Aliases