mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-02-07 02:54:16 +00:00
Compare commits
5 Commits
users/aisa
...
DE_Localiz
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b1b294c06 | ||
|
|
4d1c42f69e | ||
|
|
df6312038a | ||
|
|
e6c27f39be | ||
|
|
937989a47d |
2722
package-lock.json
generated
2722
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -51,7 +51,6 @@
|
||||
"allotment": "1.20.2",
|
||||
"applicationinsights": "1.8.0",
|
||||
"bootstrap": "3.4.1",
|
||||
"brace-expansion": "1.1.12",
|
||||
"canvas": "3.2.1",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
"clipboard-copy": "4.0.1",
|
||||
@@ -65,13 +64,14 @@
|
||||
"dayjs": "1.8.19",
|
||||
"dom-to-image": "2.6.0",
|
||||
"dotenv": "8.2.0",
|
||||
"eslint-plugin-jest": "29.12.1",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-jest": "27.4.2",
|
||||
"eslint-plugin-react": "7.33.2",
|
||||
"hasher": "1.2.0",
|
||||
"html2canvas": "1.0.0-rc.5",
|
||||
"i18next": "23.11.5",
|
||||
"i18next-browser-languagedetector": "6.0.1",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"i18next-resources-to-backend": "1.2.1",
|
||||
"iframe-resizer-react": "1.1.0",
|
||||
"immer": "9.0.6",
|
||||
"immutable": "4.0.0-rc.12",
|
||||
@@ -82,12 +82,11 @@
|
||||
"knockout": "3.5.1",
|
||||
"lodash": "4.17.23",
|
||||
"lodash-es": "4.17.23",
|
||||
"min-document": "2.19.1",
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.44.0",
|
||||
"ms": "2.1.3",
|
||||
"p-retry": "6.2.1",
|
||||
"patch-package": "8.0.1",
|
||||
"patch-package": "8.0.0",
|
||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||
"post-robot": "10.0.42",
|
||||
"q": "1.5.1",
|
||||
@@ -121,10 +120,7 @@
|
||||
},
|
||||
"overrides": {
|
||||
"d3-color": "3.1.0",
|
||||
"@octokit/plugin-paginate-rest": "9.2.2",
|
||||
"@octokit/request": "8.4.1",
|
||||
"js-yaml": "3.14.2",
|
||||
"prismjs": "1.30.0"
|
||||
"cross-spawn": "7.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.29.0",
|
||||
@@ -159,29 +155,29 @@
|
||||
"@types/styled-components": "5.1.1",
|
||||
"@types/underscore": "1.7.36",
|
||||
"@types/youtube-player": "5.5.6",
|
||||
"@typescript-eslint/eslint-plugin": "8.54.0",
|
||||
"@typescript-eslint/parser": "8.54.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.7.4",
|
||||
"@typescript-eslint/parser": "6.7.4",
|
||||
"@webpack-cli/serve": "2.0.5",
|
||||
"babel-jest": "29.7.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"buffer": "5.1.0",
|
||||
"case-sensitive-paths-webpack-plugin": "2.4.0",
|
||||
"create-file-webpack": "1.0.2",
|
||||
"css-loader": "7.1.3",
|
||||
"css-loader": "6.8.1",
|
||||
"enzyme": "3.11.0",
|
||||
"enzyme-adapter-react-16": "1.15.8",
|
||||
"enzyme-to-json": "3.6.2",
|
||||
"eslint": "9.39.2",
|
||||
"eslint": "8.50.0",
|
||||
"eslint-cli": "1.1.1",
|
||||
"eslint-plugin-no-null": "1.0.2",
|
||||
"eslint-plugin-prefer-arrow": "1.2.3",
|
||||
"eslint-plugin-react-hooks": "7.0.1",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"fast-glob": "3.2.5",
|
||||
"fs-extra": "7.0.0",
|
||||
"html-inline-css-webpack-plugin": "1.11.2",
|
||||
"html-loader": "5.0.0",
|
||||
"html-webpack-plugin": "5.5.3",
|
||||
"ip": "1.1.9",
|
||||
"i18next-resources-for-ts": "2.0.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-canvas-mock": "2.5.2",
|
||||
"jest-circus": "29.7.0",
|
||||
@@ -207,6 +203,7 @@
|
||||
"typedoc": "0.26.2",
|
||||
"typescript": "4.9.5",
|
||||
"url-loader": "4.1.1",
|
||||
"values-to-keys": "1.1.0",
|
||||
"wait-on": "9.0.3",
|
||||
"webpack": "5.104.1",
|
||||
"webpack-bundle-analyzer": "5.2.0",
|
||||
@@ -216,11 +213,11 @@
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
"start": "webpack serve --mode development",
|
||||
"start": "npm run generate:i18n-keys && webpack serve --mode development",
|
||||
"dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build",
|
||||
"build:dataExplorer:ci": "npm run build:ci",
|
||||
"build": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:prod && npm run copyToConsumers",
|
||||
"build:ci": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:fast",
|
||||
"build": "npm run generate:i18n-keys && npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:prod && npm run copyToConsumers",
|
||||
"build:ci": "npm run generate:i18n-keys && npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:fast",
|
||||
"pack:prod": "webpack --mode production",
|
||||
"pack:fast": "webpack --mode development --progress",
|
||||
"copyToConsumers": "node copyToConsumers",
|
||||
@@ -242,6 +239,7 @@
|
||||
"strict:find": "node ./strict-null-checks/find.js",
|
||||
"strict:add": "node ./strict-null-checks/auto-add.js",
|
||||
"compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks",
|
||||
"generate:i18n-keys": "node utils/generateI18nKeys.mjs",
|
||||
"generateARMClients": "npx ts-node utils/armClientGenerator/generator.ts"
|
||||
},
|
||||
"repository": {
|
||||
|
||||
11
src/@types/i18next.d.ts
vendored
Normal file
11
src/@types/i18next.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import "i18next";
|
||||
import Resources from "../Localization/en/Resources.json";
|
||||
|
||||
declare module "i18next" {
|
||||
interface CustomTypeOptions {
|
||||
defaultNS: "Resources";
|
||||
resources: {
|
||||
Resources: typeof Resources;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export function queryIterator(databaseId: string, collection: Collection, query:
|
||||
let continuationToken: string;
|
||||
return {
|
||||
fetchNext: () => {
|
||||
return queryDocuments(databaseId, collection, false, query).then((response) => {
|
||||
return queryDocuments(databaseId, collection, false, query, continuationToken).then((response) => {
|
||||
continuationToken = response.continuationToken;
|
||||
const headers: { [key: string]: string | number } = {};
|
||||
response.headers.forEach((value, key) => {
|
||||
|
||||
@@ -16,6 +16,8 @@ import { sendMessage } from "Common/MessageHandler";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { TerminalKind } from "Contracts/ViewModels";
|
||||
import { SplashScreenButton } from "Explorer/SplashScreen/SplashScreenButton";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
@@ -169,16 +171,16 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
|
||||
switch (userContext.apiType) {
|
||||
case "Postgres":
|
||||
title = "Welcome to Azure Cosmos DB for PostgreSQL";
|
||||
subtitle = "Get started with our sample datasets, documentation, and additional tools.";
|
||||
title = t(Keys.splashScreen.title.postgres);
|
||||
subtitle = t(Keys.splashScreen.subtitle.getStarted);
|
||||
break;
|
||||
case "VCoreMongo":
|
||||
title = "Welcome to Azure DocumentDB (with MongoDB compatibility)";
|
||||
subtitle = "Get started with our sample datasets, documentation, and additional tools.";
|
||||
title = t(Keys.splashScreen.title.vcoreMongo);
|
||||
subtitle = t(Keys.splashScreen.subtitle.getStarted);
|
||||
break;
|
||||
default:
|
||||
title = "Welcome to Azure Cosmos DB";
|
||||
subtitle = "Globally distributed, multi-model database service for any scale";
|
||||
title = t(Keys.splashScreen.title.default);
|
||||
subtitle = t(Keys.splashScreen.subtitle.default);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "./i18n";
|
||||
import React, { useState } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import Arrow from "../images/Arrow.svg";
|
||||
|
||||
22
src/Localization/Keys.generated.ts
Normal file
22
src/Localization/Keys.generated.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// -----------------------------------------------------------------
|
||||
// THIS FILE IS AUTO-GENERATED — DO NOT EDIT BY HAND
|
||||
// Regenerate with: npm run generate:i18n-keys
|
||||
// -----------------------------------------------------------------
|
||||
export const Keys = {
|
||||
splashScreen: {
|
||||
title: {
|
||||
/** Welcome to Azure Cosmos DB */
|
||||
default: "splashScreen.title.default",
|
||||
/** Welcome to Azure Cosmos DB for PostgreSQL */
|
||||
postgres: "splashScreen.title.postgres",
|
||||
/** Welcome to Azure DocumentDB (with MongoDB compatibility) */
|
||||
vcoreMongo: "splashScreen.title.vcoreMongo",
|
||||
},
|
||||
subtitle: {
|
||||
/** Globally distributed, multi-model database service for any scale */
|
||||
default: "splashScreen.subtitle.default",
|
||||
/** Get started with our sample datasets, documentation, and additional tools. */
|
||||
getStarted: "splashScreen.subtitle.getStarted",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
13
src/Localization/de/Resources.json
Normal file
13
src/Localization/de/Resources.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"splashScreen": {
|
||||
"title": {
|
||||
"default": "Willkommen bei Azure Cosmos DB",
|
||||
"postgres": "Willkommen bei Azure Cosmos DB für PostgreSQL",
|
||||
"vcoreMongo": "Willkommen bei Azure DocumentDB (mit MongoDB-Kompatibilität)"
|
||||
},
|
||||
"subtitle": {
|
||||
"default": "Global verteilter, multimodaler Datenbankdienst für jede Skalierung",
|
||||
"getStarted": "Beginnen Sie mit unseren Beispieldatensätzen, Dokumentation und zusätzlichen Tools."
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Localization/en/Resources.json
Normal file
13
src/Localization/en/Resources.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"splashScreen": {
|
||||
"title": {
|
||||
"default": "Welcome to Azure Cosmos DB",
|
||||
"postgres": "Welcome to Azure Cosmos DB for PostgreSQL",
|
||||
"vcoreMongo": "Welcome to Azure DocumentDB (with MongoDB compatibility)"
|
||||
},
|
||||
"subtitle": {
|
||||
"default": "Globally distributed, multi-model database service for any scale",
|
||||
"getStarted": "Get started with our sample datasets, documentation, and additional tools."
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/Localization/t.ts
Normal file
24
src/Localization/t.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import i18n from "../i18n";
|
||||
import type enResources from "./en/Resources.json";
|
||||
|
||||
/**
|
||||
* Derives a union of all dot-notation key paths from a nested JSON object type.
|
||||
* e.g. { buttons: { save: "Save" } } → "buttons.save"
|
||||
*/
|
||||
type NestedKeyOf<T, P extends string = ""> = {
|
||||
[K in keyof T & string]: T[K] extends Record<string, unknown>
|
||||
? NestedKeyOf<T[K], P extends "" ? K : `${P}.${K}`>
|
||||
: P extends ""
|
||||
? K
|
||||
: `${P}.${K}`;
|
||||
}[keyof T & string];
|
||||
|
||||
/** All valid translation keys derived from en/Resources.json */
|
||||
export type ResourceKey = NestedKeyOf<typeof enResources>;
|
||||
|
||||
/**
|
||||
* Type-safe translation function bound to the "Resources" namespace.
|
||||
* Use this everywhere—class components, functional components, and non-React code.
|
||||
*/
|
||||
export const t = (key: ResourceKey, options?: Record<string, unknown>): string =>
|
||||
(i18n.t as (key: string, options?: unknown) => string)(key, { ns: "Resources", ...options });
|
||||
@@ -1,16 +1,21 @@
|
||||
import i18n from "i18next";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import resourcesToBackend from "i18next-resources-to-backend";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(resourcesToBackend((lng: string, ns: string) => import(`./Localization/${lng}/${ns}.json`)))
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: "en",
|
||||
defaultNS: "Resources",
|
||||
ns: ["Resources"],
|
||||
detection: { order: ["navigator", "cookie", "localStorage", "sessionStorage", "querystring", "htmlTag"] },
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
keySeparator: ".",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
formatSeparator: ",",
|
||||
},
|
||||
react: {
|
||||
|
||||
@@ -2,20 +2,54 @@ import { Page } from "@playwright/test";
|
||||
|
||||
export async function setupCORSBypass(page: Page) {
|
||||
await page.route("**/api/mongo/explorer{,/**}", async (route) => {
|
||||
const request = route.request();
|
||||
const origin = request.headers()["origin"];
|
||||
|
||||
// If there's no origin, it's not a CORS request. Let it proceed without modification.
|
||||
if (!origin) {
|
||||
await route.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
//// Handle preflight (OPTIONS) requests separately.
|
||||
// These should not be forwarded to the target server.
|
||||
if (request.method() === "OPTIONS") {
|
||||
await route.fulfill({
|
||||
status: 204, // No Content
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": origin,
|
||||
"Access-Control-Allow-Credentials": "true",
|
||||
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS,HEAD",
|
||||
"Access-Control-Request-Headers": "*, x-ms-continuation",
|
||||
"Access-Control-Max-Age": "86400", // Cache preflight response for 1 day
|
||||
Vary: "Origin",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the actual GET/POST request
|
||||
const response = await route.fetch({
|
||||
headers: {
|
||||
...route.request().headers(),
|
||||
...request.headers(),
|
||||
},
|
||||
});
|
||||
|
||||
const responseHeaders = response.headers();
|
||||
// Clean up any pre-existing CORS headers from the real response to avoid conflicts.
|
||||
delete responseHeaders["access-control-allow-origin"];
|
||||
delete responseHeaders["access-control-allow-credentials"];
|
||||
|
||||
await route.fulfill({
|
||||
status: response.status(),
|
||||
headers: {
|
||||
...response.headers(),
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "*",
|
||||
...responseHeaders,
|
||||
"Access-Control-Allow-Origin": origin,
|
||||
"Access-Control-Allow-Credentials": "true",
|
||||
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS,HEAD",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
"Access-Control-Allow-Credentials": "*",
|
||||
"Access-Control-Expose-Headers": "x-ms-continuation,x-ms-request-charge,x-ms-session-token",
|
||||
Vary: "Origin",
|
||||
},
|
||||
body: await response.body(),
|
||||
});
|
||||
|
||||
132
test/mongo/pagination.spec.ts
Normal file
132
test/mongo/pagination.spec.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { setupCORSBypass } from "../CORSBypass";
|
||||
import { DataExplorer, QueryTab, TestAccount, CommandBarButton, Editor } from "../fx";
|
||||
import { serializeMongoToJson } from "../testData";
|
||||
|
||||
const databaseId = "test-e2etests-mongo-pagination";
|
||||
const collectionId = "test-coll-mongo-pagination";
|
||||
let explorer: DataExplorer = null!;
|
||||
|
||||
test.setTimeout(5 * 60 * 1000);
|
||||
|
||||
test.describe("Test Mongo Pagination", () => {
|
||||
let queryTab: QueryTab;
|
||||
let queryEditor: Editor;
|
||||
|
||||
test.beforeEach("Open query tab", async ({ page }) => {
|
||||
await setupCORSBypass(page);
|
||||
explorer = await DataExplorer.open(page, TestAccount.MongoReadonly);
|
||||
|
||||
const containerNode = await explorer.waitForContainerNode(databaseId, collectionId);
|
||||
await containerNode.expand();
|
||||
|
||||
const containerMenuNode = await explorer.waitForContainerDocumentsNode(databaseId, collectionId);
|
||||
await containerMenuNode.openContextMenu();
|
||||
await containerMenuNode.contextMenuItem("New Query").click();
|
||||
|
||||
queryTab = explorer.queryTab("tab0");
|
||||
queryEditor = queryTab.editor();
|
||||
await queryEditor.locator.waitFor({ timeout: 30 * 1000 });
|
||||
await queryTab.executeCTA.waitFor();
|
||||
await explorer.frame.getByTestId("NotificationConsole/ExpandCollapseButton").click();
|
||||
await explorer.frame.getByTestId("NotificationConsole/Contents").waitFor();
|
||||
});
|
||||
|
||||
test("should execute a query and load more results", async ({ page }) => {
|
||||
const query = "{}";
|
||||
|
||||
await queryEditor.locator.click();
|
||||
await queryEditor.setText(query);
|
||||
|
||||
const executeQueryButton = explorer.commandBarButton(CommandBarButton.ExecuteQuery);
|
||||
await executeQueryButton.click();
|
||||
|
||||
// Wait for query execution to complete
|
||||
await expect(queryTab.resultsView).toBeVisible({ timeout: 60000 });
|
||||
await expect(queryTab.resultsEditor.locator).toBeAttached({ timeout: 30000 });
|
||||
|
||||
// Get initial results
|
||||
const resultText = await queryTab.resultsEditor.text();
|
||||
|
||||
if (!resultText || resultText.trim() === "" || resultText.trim() === "[]") {
|
||||
throw new Error("Query returned no results - the collection appears to be empty");
|
||||
}
|
||||
|
||||
const resultData = serializeMongoToJson(resultText);
|
||||
|
||||
if (resultData.length === 0) {
|
||||
throw new Error("Parsed results contain 0 documents - collection is empty");
|
||||
}
|
||||
|
||||
if (resultData.length < 100) {
|
||||
expect(resultData.length).toBeGreaterThan(0);
|
||||
return;
|
||||
}
|
||||
|
||||
expect(resultData.length).toBe(100);
|
||||
|
||||
// Pagination test
|
||||
let totalPagesLoaded = 1;
|
||||
const maxLoadMoreAttempts = 10;
|
||||
|
||||
for (let loadMoreAttempts = 0; loadMoreAttempts < maxLoadMoreAttempts; loadMoreAttempts++) {
|
||||
const loadMoreButton = queryTab.resultsView.getByText("Load more");
|
||||
|
||||
try {
|
||||
await expect(loadMoreButton).toBeVisible({ timeout: 5000 });
|
||||
} catch {
|
||||
// Load more button not visible - pagination complete
|
||||
break;
|
||||
}
|
||||
|
||||
const beforeClickText = await queryTab.resultsEditor.text();
|
||||
const beforeClickHash = Buffer.from(beforeClickText || "")
|
||||
.toString("base64")
|
||||
.substring(0, 50);
|
||||
|
||||
await loadMoreButton.click();
|
||||
|
||||
// Wait for content to update
|
||||
let editorContentChanged = false;
|
||||
for (let waitAttempt = 1; waitAttempt <= 3; waitAttempt++) {
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const currentEditorText = await queryTab.resultsEditor.text();
|
||||
const currentHash = Buffer.from(currentEditorText || "")
|
||||
.toString("base64")
|
||||
.substring(0, 50);
|
||||
|
||||
if (currentHash !== beforeClickHash) {
|
||||
editorContentChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (editorContentChanged) {
|
||||
totalPagesLoaded++;
|
||||
} else {
|
||||
// No content change detected, stop pagination
|
||||
break;
|
||||
}
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
// Final verification
|
||||
const finalIndicator = queryTab.resultsView.locator("text=/\\d+ - \\d+/");
|
||||
const finalIndicatorText = await finalIndicator.textContent();
|
||||
|
||||
if (finalIndicatorText) {
|
||||
const match = finalIndicatorText.match(/(\d+) - (\d+)/);
|
||||
if (match) {
|
||||
const totalDocuments = parseInt(match[2]);
|
||||
expect(totalDocuments).toBe(405);
|
||||
expect(totalPagesLoaded).toBe(5);
|
||||
} else {
|
||||
throw new Error(`Invalid results indicator format: ${finalIndicatorText}`);
|
||||
}
|
||||
} else {
|
||||
expect(totalPagesLoaded).toBe(5);
|
||||
}
|
||||
});
|
||||
});
|
||||
71
utils/generateI18nKeys.mjs
Normal file
71
utils/generateI18nKeys.mjs
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Generates src/Localization/Keys.generated.ts from en/Resources.json.
|
||||
*
|
||||
* Every leaf value becomes its dot-notation key path, with JSDoc annotations
|
||||
* showing the English translation so developers see real text on hover.
|
||||
*
|
||||
* Libraries:
|
||||
* - values-to-keys — replaces translation values with dot-path keys
|
||||
* - i18next-resources-for-ts (json2ts) — serialises objects as typed `as const` TS
|
||||
*
|
||||
* Usage: node utils/generateI18nKeys.mjs
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
import { json2ts } from "i18next-resources-for-ts";
|
||||
import { dirname, resolve } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { replace } from "values-to-keys";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = resolve(__dirname, "..");
|
||||
const INPUT = resolve(ROOT, "src/Localization/en/Resources.json");
|
||||
const OUTPUT = resolve(ROOT, "src/Localization/Keys.generated.ts");
|
||||
|
||||
// ── helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Walk two parallel objects (keyed + original) and produce TS source
|
||||
* with JSDoc comments showing the English value at every leaf.
|
||||
*/
|
||||
function serialiseWithJSDoc(obj, orig, indent = 2) {
|
||||
const pad = " ".repeat(indent);
|
||||
const lines = ["{"];
|
||||
for (const key of Object.keys(obj)) {
|
||||
const val = obj[key];
|
||||
const origVal = orig[key];
|
||||
if (typeof val === "object" && val !== null) {
|
||||
lines.push(`${pad}${key}: ${serialiseWithJSDoc(val, origVal, indent + 2)},`);
|
||||
} else {
|
||||
lines.push(`${pad}/** ${origVal} */`);
|
||||
lines.push(`${pad}${key}: ${JSON.stringify(val)},`);
|
||||
}
|
||||
}
|
||||
lines.push(`${" ".repeat(Math.max(0, indent - 2))}}`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// ── main ───────────────────────────────────────────────────────────
|
||||
|
||||
// Keep the original English values for JSDoc annotations
|
||||
const original = JSON.parse(readFileSync(INPUT, "utf-8"));
|
||||
|
||||
// Use values-to-keys to replace every leaf value with its dot-path key
|
||||
const keyed = replace(JSON.parse(readFileSync(INPUT, "utf-8")));
|
||||
|
||||
// Use json2ts to verify the shape is valid for `as const` export
|
||||
// (We still use our own serialiser because json2ts doesn't add JSDoc comments)
|
||||
json2ts(keyed); // validates structure; throws on malformed input
|
||||
|
||||
const banner = `\
|
||||
// -----------------------------------------------------------------
|
||||
// THIS FILE IS AUTO-GENERATED — DO NOT EDIT BY HAND
|
||||
// Regenerate with: npm run generate:i18n-keys
|
||||
// -----------------------------------------------------------------
|
||||
`;
|
||||
|
||||
const body = `export const Keys = ${serialiseWithJSDoc(keyed, original)} as const;\n`;
|
||||
|
||||
writeFileSync(OUTPUT, banner + body, "utf-8");
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Generated ${OUTPUT}`);
|
||||
Reference in New Issue
Block a user