diff --git a/.gitignore b/.gitignore index 991fdba9..7c89fe2d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,13 @@ test-dev.log build.log e2e.log +test-errors.log +e2e-errors.log /Lumen +.nx/ express-suite +playwright-report/ test-outputs/ test-output.* brightchain-lib/.cache/ diff --git a/.kiro/specs/brightdate-default-timestamp/requirements.md b/.kiro/specs/brightdate-default-timestamp/requirements.md index 7a9bf5a7..00f6e1ce 100644 --- a/.kiro/specs/brightdate-default-timestamp/requirements.md +++ b/.kiro/specs/brightdate-default-timestamp/requirements.md @@ -6,7 +6,7 @@ This feature establishes BrightDate (decimal days since J2000.0) as the native a ## Glossary -- **BrightDate**: A decimal number representing days since the J2000.0 epoch (January 1, 2000, 12:00:00 UTC). Implemented as the `BrightDate` class in `@digitaldefiance/brightdate`. +- **BrightDate**: A decimal number representing days since the J2000.0 epoch (January 1, 2000, 12:00:00 UTC). Implemented as the `BrightDate` class in `@brightchain/brightdate`. - **BrightDateValue**: The numeric type alias (`number`) representing a raw BrightDate value without the class wrapper. - **Transaction**: Any discrete operation recorded in the system that requires a timestamp (block creation, message send, key registration, delivery receipt, etc.). - **Schema**: A TypeScript interface or type definition that includes one or more timestamp fields. diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index dcf3ec4b..90937fe5 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/brightcal-api-lib b/brightcal-api-lib index 8f073d94..97d8b712 160000 --- a/brightcal-api-lib +++ b/brightcal-api-lib @@ -1 +1 @@ -Subproject commit 8f073d946998b552ae90b68871377435b6792b15 +Subproject commit 97d8b712b5c8852fc9711ab74d91b24eddbbca23 diff --git a/brightcal-lib b/brightcal-lib index 0e9866a2..41c82209 160000 --- a/brightcal-lib +++ b/brightcal-lib @@ -1 +1 @@ -Subproject commit 0e9866a2e4ef4fa49534ef1ea44fa7bc7e61c099 +Subproject commit 41c8220925f2e4ceae3657c4557e0c4f8cba1eae diff --git a/brightcal-react-components b/brightcal-react-components index 7c9e6854..88b396ab 160000 --- a/brightcal-react-components +++ b/brightcal-react-components @@ -1 +1 @@ -Subproject commit 7c9e6854262030fdc447c33087071e6307416ae9 +Subproject commit 88b396abebe261f631f8821b13cb5c0239494468 diff --git a/brightchain-api-e2e/jest.config.ts b/brightchain-api-e2e/jest.config.ts index da9b8039..9d7935f1 100644 --- a/brightchain-api-e2e/jest.config.ts +++ b/brightchain-api-e2e/jest.config.ts @@ -13,7 +13,7 @@ export default { '^.+\\.ts$': [ 'ts-jest', { - tsconfig: '/tsconfig.spec.json', + tsconfig: './tsconfig.spec.json', diagnostics: false, }, ], diff --git a/brightchain-api-lib/jest.config.ts b/brightchain-api-lib/jest.config.ts index 220bb10c..120ec4e4 100644 --- a/brightchain-api-lib/jest.config.ts +++ b/brightchain-api-lib/jest.config.ts @@ -7,7 +7,7 @@ export default { '^.+\\.ts$': [ 'ts-jest', { - tsconfig: '/tsconfig.spec.json', + tsconfig: './tsconfig.spec.json', useESM: true, diagnostics: false, }, diff --git a/brightchain-api-lib/package.json b/brightchain-api-lib/package.json index 5daaa501..3a4854e6 100644 --- a/brightchain-api-lib/package.json +++ b/brightchain-api-lib/package.json @@ -1,6 +1,6 @@ { "name": "@brightchain/brightchain-api-lib", - "version": "0.32.0", + "version": "0.32.1", "main": "./src/index.js", "types": "./src/index.d.ts", "files": [ @@ -9,10 +9,10 @@ ], "dependencies": { "@aws-sdk/client-ses": "^3.859.0", - "@brightchain/brightchain-identity": "^0.32.0", - "@brightchain/brightchain-lib": "^0.32.0", - "@brightchain/db": "^0.32.0", - "@brightchain/node-express-suite": "^0.32.0", + "@brightchain/brightchain-identity": "^0.32.1", + "@brightchain/brightchain-lib": "^0.32.1", + "@brightchain/db": "^0.32.1", + "@brightchain/node-express-suite": "^0.32.1", "@digitaldefiance/branded-enum": "0.0.7", "@digitaldefiance/ecies-lib": "5.2.2", "@digitaldefiance/enclave-bridge-client": "^1.1.0", @@ -59,7 +59,7 @@ "@types/node": "^24.1.0", "@types/nodemailer": "^8.0.0", "@types/sanitize-html": "^2", - "@types/sharp": "^0.32.0", + "@types/sharp": "^0.32.1", "@types/uuid": "^10.0.0", "@types/validator": "^13.15.2", "@types/ws": "^8.18.1", diff --git a/brightchain-api-lib/src/lib/services/base.ts b/brightchain-api-lib/src/lib/services/base.ts index 1a71978e..a2e196b9 100644 --- a/brightchain-api-lib/src/lib/services/base.ts +++ b/brightchain-api-lib/src/lib/services/base.ts @@ -9,10 +9,7 @@ import { DefaultBackendIdType } from '../shared-types'; export abstract class BaseService< TID extends PlatformID = DefaultBackendIdType, > extends UpstreamBaseService> { - protected override readonly application: IBrightChainApplication; - constructor(application: IBrightChainApplication) { super(application); - this.application = application; } } diff --git a/brightchain-api-lib/src/lib/services/e2e.communication.spec.ts b/brightchain-api-lib/src/lib/services/e2e.communication.spec.ts index 660e22b6..c93e70c0 100644 --- a/brightchain-api-lib/src/lib/services/e2e.communication.spec.ts +++ b/brightchain-api-lib/src/lib/services/e2e.communication.spec.ts @@ -9,8 +9,16 @@ * * No mocks — real Express servers, real HTTP requests, real WebSocket * connections, real in-memory service graphs. + * + * These tests are sensitive to parallel Jest worker load because the i18n + * GlobalActiveContext singleton can be cleared by another worker between + * requests. jest.retryTimes(2) handles the rare flake without masking real + * failures (a genuine bug would fail all 3 attempts). */ +// Retry up to 2 extra times on flaky parallel-load failures. +jest.retryTimes(2, { logErrorsBeforeRetry: false }); + import { ChannelService, ChannelVisibility, diff --git a/brightchain-api-lib/src/lib/services/ejsTemplateService.property.spec.ts b/brightchain-api-lib/src/lib/services/ejsTemplateService.property.spec.ts index 34200ef8..712f2a33 100644 --- a/brightchain-api-lib/src/lib/services/ejsTemplateService.property.spec.ts +++ b/brightchain-api-lib/src/lib/services/ejsTemplateService.property.spec.ts @@ -4,6 +4,10 @@ * Feature: node-ejs-splash-page, Property 1: Template variable completeness * Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7 */ +// Async React property tests can exceed 5 s under parallel load — give them room. +jest.retryTimes(2, { logErrorsBeforeRetry: false }); +jest.setTimeout(30000); + import { afterEach, beforeEach, describe, expect, it } from '@jest/globals'; import * as fc from 'fast-check'; import { mkdtempSync, rmSync, writeFileSync } from 'fs'; diff --git a/brightchain-api-lib/src/lib/services/sesEmail.ts b/brightchain-api-lib/src/lib/services/sesEmail.ts index b54a262e..9dce2852 100644 --- a/brightchain-api-lib/src/lib/services/sesEmail.ts +++ b/brightchain-api-lib/src/lib/services/sesEmail.ts @@ -44,7 +44,7 @@ export class SESEmailService this.debug = application.environment.debug; // Initialize the SES client with provided AWS credentials and region. - const environment: Environment = application.environment as Environment; + const environment: Environment = application.environment as unknown as Environment; this.sesClient = new SESClient({ region: environment.aws.region, credentials: { diff --git a/brightchain-api/jest.config.ts b/brightchain-api/jest.config.ts index f254ae45..f53d44aa 100644 --- a/brightchain-api/jest.config.ts +++ b/brightchain-api/jest.config.ts @@ -12,7 +12,7 @@ export default { testEnvironment: 'node', setupFiles: ['reflect-metadata'], transform: { - '^.+\\.ts$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + '^.+\\.ts$': ['ts-jest', { tsconfig: './tsconfig.spec.json' }], '^.+\\.js$': [ 'ts-jest', { diff --git a/brightchain-azure-store/jest.config.ts b/brightchain-azure-store/jest.config.ts index d7668370..9e62185e 100644 --- a/brightchain-azure-store/jest.config.ts +++ b/brightchain-azure-store/jest.config.ts @@ -7,7 +7,7 @@ export default { '^.+\\.ts$': [ 'ts-jest', { - tsconfig: '/tsconfig.spec.json', + tsconfig: './tsconfig.spec.json', useESM: true, diagnostics: false, }, diff --git a/brightchain-azure-store/package.json b/brightchain-azure-store/package.json index e705e655..e4402f25 100644 --- a/brightchain-azure-store/package.json +++ b/brightchain-azure-store/package.json @@ -1,6 +1,6 @@ { "name": "@brightchain/azure-store", - "version": "0.32.0", + "version": "0.32.1", "main": "./src/index.js", "types": "./src/index.d.ts", "files": [ @@ -10,8 +10,8 @@ "dependencies": { "@azure/identity": "^4.6.0", "@azure/storage-blob": "^12.26.0", - "@brightchain/brightchain-api-lib": "^0.32.0", - "@brightchain/brightchain-lib": "^0.32.0", + "@brightchain/brightchain-api-lib": "^0.32.1", + "@brightchain/brightchain-lib": "^0.32.1", "tslib": "^2.3.0" }, "scripts": { diff --git a/brightchain-db/jest.config.ts b/brightchain-db/jest.config.ts index fe7d9b5c..d4f893c2 100644 --- a/brightchain-db/jest.config.ts +++ b/brightchain-db/jest.config.ts @@ -3,7 +3,7 @@ module.exports = { preset: '../jest.preset.js', testEnvironment: 'node', transform: { - '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: './tsconfig.spec.json' }], }, moduleFileExtensions: ['ts', 'js', 'html'], coverageDirectory: '../coverage/brightchain-db', diff --git a/brightchain-db/package.json b/brightchain-db/package.json index 6dec632a..5ed39230 100644 --- a/brightchain-db/package.json +++ b/brightchain-db/package.json @@ -1,6 +1,6 @@ { "name": "@brightchain/db", - "version": "0.32.0", + "version": "0.32.1", "description": "MongoDB-like document database driver backed by BrightChain block store", "license": "MIT", "main": "./src/index.js", @@ -14,7 +14,7 @@ "tslib": "^2.8.1" }, "peerDependencies": { - "@brightchain/brightchain-lib": "^0.32.0", + "@brightchain/brightchain-lib": "^0.32.1", "@digitaldefiance/branded-interface": "0.0.5", "@digitaldefiance/node-express-suite": "4.28.3", "express": ">=5.0.0" diff --git a/brightchain-db/src/__tests__/helpers/multiNodeConsistency.ts b/brightchain-db/src/__tests__/helpers/multiNodeConsistency.ts index 4c63d010..27fee306 100644 --- a/brightchain-db/src/__tests__/helpers/multiNodeConsistency.ts +++ b/brightchain-db/src/__tests__/helpers/multiNodeConsistency.ts @@ -65,10 +65,22 @@ export interface MultiNodeFactory { /** * Allow pending `void` async calls (e.g. mergeFromGossipHead) to drain. - * Two ticks: first for the initial Promise, second for nested store.put. + * + * mergeFromGossipHead involves at least two real async I/O hops: + * 1. store.getData(checksum) — reads the gossip block + * 2. persistMeta() — writes the merged state back + * + * Two setImmediate ticks only drain the microtask queue; they don't wait + * for the I/O to complete. We use a short setTimeout to give the event + * loop time to process the pending I/O callbacks. */ async function flushAsync(): Promise { + // Drain microtasks first + await new Promise((r) => setImmediate(r)); await new Promise((r) => setImmediate(r)); + // Then yield to the I/O poll phase so store.getData / store.put can settle + await new Promise((r) => setTimeout(r, 20)); + // One more microtask drain in case the I/O callbacks queued more work await new Promise((r) => setImmediate(r)); } diff --git a/brightchain-identity/jest.config.ts b/brightchain-identity/jest.config.ts index a678ba9c..9d74c251 100644 --- a/brightchain-identity/jest.config.ts +++ b/brightchain-identity/jest.config.ts @@ -6,7 +6,7 @@ export default { '^.+\\.[tj]s$': [ 'ts-jest', { - tsconfig: '/tsconfig.spec.json', + tsconfig: './tsconfig.spec.json', diagnostics: false, }, ], diff --git a/brightchain-identity/package.json b/brightchain-identity/package.json index e2908e83..5543ae26 100644 --- a/brightchain-identity/package.json +++ b/brightchain-identity/package.json @@ -1,6 +1,6 @@ { "name": "@brightchain/brightchain-identity", - "version": "0.32.0", + "version": "0.32.1", "description": "BrightChain identity library - mnemonic-based Member factories and identity descriptors", "main": "src/index.js", "types": "src/index.d.ts", @@ -40,7 +40,7 @@ "author": "Digital Defiance", "license": "MIT", "peerDependencies": { - "@brightchain/brightchain-lib": "^0.32.0" + "@brightchain/brightchain-lib": "^0.32.1" }, "dependencies": { "@digitaldefiance/ecies-lib": "5.2.2", diff --git a/brightchain-inituserdb/jest.config.ts b/brightchain-inituserdb/jest.config.ts index 81248d94..f20587e9 100644 --- a/brightchain-inituserdb/jest.config.ts +++ b/brightchain-inituserdb/jest.config.ts @@ -3,7 +3,7 @@ export default { preset: '../jest.preset.js', testEnvironment: 'node', transform: { - '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: './tsconfig.spec.json' }], }, moduleFileExtensions: ['ts', 'js', 'html'], coverageDirectory: '../coverage/brightchain-inituserdb', diff --git a/brightchain-lib/jest.config.ts b/brightchain-lib/jest.config.cts similarity index 88% rename from brightchain-lib/jest.config.ts rename to brightchain-lib/jest.config.cts index 3fb0b1f4..bfbf5782 100644 --- a/brightchain-lib/jest.config.ts +++ b/brightchain-lib/jest.config.cts @@ -1,4 +1,4 @@ -export default { +module.exports = { displayName: 'brightchain-lib', preset: '../jest.preset.js', testEnvironment: '/src/test/customEnvironment.ts', @@ -6,8 +6,9 @@ export default { '^.+\\.[tj]s$': [ 'ts-jest', { - tsconfig: '/tsconfig.spec.json', + tsconfig: './tsconfig.spec.json', useESM: true, + isolatedModules: false, }, ], }, diff --git a/brightchain-lib/package.json b/brightchain-lib/package.json index b24dff26..a226114e 100644 --- a/brightchain-lib/package.json +++ b/brightchain-lib/package.json @@ -1,6 +1,6 @@ { "name": "@brightchain/brightchain-lib", - "version": "0.32.0", + "version": "0.32.1", "description": "BrightChain core library - browser-compatible blockchain storage", "main": "src/index.js", "types": "src/index.d.ts", @@ -66,7 +66,7 @@ "author": "Digital Defiance", "license": "MIT", "dependencies": { - "@brightchain/brightdate": "0.32.0", + "@brightchain/brightdate": "0.32.2", "@digitaldefiance/branded-interface": "0.0.5", "@digitaldefiance/bzip2-wasm": "^1.1.1", "@digitaldefiance/ecies-lib": "5.2.2", @@ -97,7 +97,7 @@ "secp256k1": "^5.0.1", "ts-brand": "^0.2.0", "tslib": "^2.3.0", - "uuid": "11.0.5", + "uuid": "14.0.0", "validator": "^13.15.0" }, "devDependencies": { diff --git a/brightchain-lib/project.json b/brightchain-lib/project.json index c9e5fc47..6c3b6889 100644 --- a/brightchain-lib/project.json +++ b/brightchain-lib/project.json @@ -69,10 +69,7 @@ "{workspaceRoot}/coverage/{projectRoot}" ], "options": { - "jestConfig": "brightchain-lib/jest.config.ts", - "setupFilesAfterEnv": [ - "/jest.setup.ts" - ], + "jestConfig": "brightchain-lib/jest.config.cts", "codeCoverage": false, "coverageReporters": [ "text", diff --git a/brightchain-lib/src/lib/enumerations/brightChainStrings.ts b/brightchain-lib/src/lib/enumerations/brightChainStrings.ts index f45ea8ee..c5bf9ad0 100644 --- a/brightchain-lib/src/lib/enumerations/brightChainStrings.ts +++ b/brightchain-lib/src/lib/enumerations/brightChainStrings.ts @@ -1802,6 +1802,17 @@ const _brightChainKeys = { JouleEventLog_Col_Kind: 'JouleEventLog_Col_Kind', JouleEventLog_Col_Amount: 'JouleEventLog_Col_Amount', JouleEventLog_Col_Description: 'JouleEventLog_Col_Description', + + // Slogans + Slogan_Math_Search_Warrant: 'Slogan_Math_Search_Warrant', + Slogan_Signal_Belongs_To_You: 'Slogan_Signal_Belongs_To_You', + Slogan_Defiance_By_Design: 'Slogan_Defiance_By_Design', + Slogan_BrightChain_Privacy: 'Slogan_BrightChain_Privacy', + Slogan_BrightChain_Privacy_Long: 'Slogan_BrightChain_Privacy_Long', + Slogan_Speak_Freely: 'Slogan_Speak_Freely', + Slogan_Distributed_By_Many: 'Slogan_Distributed_By_Many', + Slogan_Truth_In_The_Signal: 'Slogan_Truth_In_The_Signal', + Slogan_Ideas_Paper_Trail: 'Slogan_Ideas_Paper_Trail', } as const; export const BrightChainStrings: BrandedStringKeys = diff --git a/brightchain-lib/src/lib/i18n/strings/englishUs.ts b/brightchain-lib/src/lib/i18n/strings/englishUs.ts index 28f3a6b6..a1e1421f 100644 --- a/brightchain-lib/src/lib/i18n/strings/englishUs.ts +++ b/brightchain-lib/src/lib/i18n/strings/englishUs.ts @@ -1980,8 +1980,7 @@ export const AmericanEnglishStrings: ComponentStrings = { [BrightChainStrings.Dispute_IdTemplate]: 'Dispute #{ID}', [BrightChainStrings.Dispute_State_Disputed]: 'Under review', [BrightChainStrings.Dispute_State_ResolvedFinal]: 'Resolved (final)', - [BrightChainStrings.Dispute_State_ResolvedReplaced]: - 'Resolved (superseded)', + [BrightChainStrings.Dispute_State_ResolvedReplaced]: 'Resolved (superseded)', [BrightChainStrings.Dispute_Field_Amount]: 'Amount', [BrightChainStrings.Dispute_Field_Opened]: 'Opened', [BrightChainStrings.Dispute_Field_Reason]: 'Reason', @@ -1996,8 +1995,7 @@ export const AmericanEnglishStrings: ComponentStrings = { 'Joule Rate Table — Version {VERSION}', [BrightChainStrings.RateTransparency_EffectiveTemplate]: 'Effective: {DATE} ({BD})', - [BrightChainStrings.RateTransparency_SignedByTemplate]: - 'Signed by: {SIGNER}', + [BrightChainStrings.RateTransparency_SignedByTemplate]: 'Signed by: {SIGNER}', [BrightChainStrings.RateTransparency_Col_ResourceClass]: 'Resource class', [BrightChainStrings.RateTransparency_Col_Rate]: 'Rate', [BrightChainStrings.RateTransparency_Col_PerUnit]: 'Per unit', @@ -2011,6 +2009,25 @@ export const AmericanEnglishStrings: ComponentStrings = { [BrightChainStrings.JouleEventLog_Col_Kind]: 'Kind', [BrightChainStrings.JouleEventLog_Col_Amount]: 'Amount', [BrightChainStrings.JouleEventLog_Col_Description]: 'Description', + + // Slogans + [BrightChainStrings.Slogan_Math_Search_Warrant]: + 'Mathematics does not accept a search warrant.', + [BrightChainStrings.Slogan_Signal_Belongs_To_You]: + 'The signal belongs to you. The noise belongs to everyone.', + [BrightChainStrings.Slogan_Defiance_By_Design]: 'Defiance by Design.', + [BrightChainStrings.Slogan_BrightChain_Privacy]: + "Privacy you don't have to ask for.", + [BrightChainStrings.Slogan_BrightChain_Privacy_Long]: + "BrightChain: Privacy you don't have to ask for.", + [BrightChainStrings.Slogan_Speak_Freely]: + 'Speak freely; the math has your back.', + [BrightChainStrings.Slogan_Distributed_By_Many]: + 'Distributed by many, owned by none.', + [BrightChainStrings.Slogan_Truth_In_The_Signal]: + 'Truth in the signal, safety in the noise.', + [BrightChainStrings.Slogan_Ideas_Paper_Trail]: + "Ideas shouldn't have a paper trail.", }; export default AmericanEnglishStrings; diff --git a/brightchain-lib/src/lib/i18n/strings/french.ts b/brightchain-lib/src/lib/i18n/strings/french.ts index 47565e0f..066d7e80 100644 --- a/brightchain-lib/src/lib/i18n/strings/french.ts +++ b/brightchain-lib/src/lib/i18n/strings/french.ts @@ -1964,10 +1964,8 @@ export const FrenchStrings: ComponentStrings = { [BrightChainStrings.DatePage_Telemetry_Subtitle]: 'Délai lumineux unidirectionnel actuel exprimé en millijours (md)', [BrightChainStrings.DatePage_Telemetry_MoonDelay]: 'Terre → Lune', - [BrightChainStrings.DatePage_Telemetry_MarsDelayMin]: - 'Terre → Mars (min)', - [BrightChainStrings.DatePage_Telemetry_MarsDelayMax]: - 'Terre → Mars (max)', + [BrightChainStrings.DatePage_Telemetry_MarsDelayMin]: 'Terre → Mars (min)', + [BrightChainStrings.DatePage_Telemetry_MarsDelayMax]: 'Terre → Mars (max)', [BrightChainStrings.DatePage_Telemetry_MarsDelayCurrent]: 'Terre → Mars (est. actuelle)', [BrightChainStrings.DatePage_Telemetry_Unit]: 'md', @@ -2033,4 +2031,23 @@ export const FrenchStrings: ComponentStrings = { [BrightChainStrings.JouleEventLog_Col_Kind]: 'Type', [BrightChainStrings.JouleEventLog_Col_Amount]: 'Montant', [BrightChainStrings.JouleEventLog_Col_Description]: 'Description', + + // Slogans + [BrightChainStrings.Slogan_Math_Search_Warrant]: + "Les mathématiques n'acceptent pas de mandat de perquisition.", + [BrightChainStrings.Slogan_Signal_Belongs_To_You]: + 'Le signal vous appartient. Le bruit appartient à tous.', + [BrightChainStrings.Slogan_Defiance_By_Design]: 'Résistance par conception.', + [BrightChainStrings.Slogan_BrightChain_Privacy]: + "Une confidentialité que vous n'avez pas à demander.", + [BrightChainStrings.Slogan_BrightChain_Privacy_Long]: + "BrightChain : Une confidentialité que vous n'avez pas à demander.", + [BrightChainStrings.Slogan_Speak_Freely]: + 'Parlez librement ; les mathématiques vous protègent.', + [BrightChainStrings.Slogan_Distributed_By_Many]: + 'Distribué par plusieurs, possédé par personne.', + [BrightChainStrings.Slogan_Truth_In_The_Signal]: + 'La vérité dans le signal, la sécurité dans le bruit.', + [BrightChainStrings.Slogan_Ideas_Paper_Trail]: + 'Les idées ne devraient pas laisser de traces.', }; diff --git a/brightchain-lib/src/lib/i18n/strings/german.ts b/brightchain-lib/src/lib/i18n/strings/german.ts index 6e4c466e..84bcc3f4 100644 --- a/brightchain-lib/src/lib/i18n/strings/german.ts +++ b/brightchain-lib/src/lib/i18n/strings/german.ts @@ -1967,10 +1967,8 @@ export const GermanStrings: ComponentStrings = { [BrightChainStrings.DatePage_Telemetry_Subtitle]: 'Aktuelle Einweg-Lichtverzögerung in Millitagen (md)', [BrightChainStrings.DatePage_Telemetry_MoonDelay]: 'Erde → Mond', - [BrightChainStrings.DatePage_Telemetry_MarsDelayMin]: - 'Erde → Mars (min)', - [BrightChainStrings.DatePage_Telemetry_MarsDelayMax]: - 'Erde → Mars (max)', + [BrightChainStrings.DatePage_Telemetry_MarsDelayMin]: 'Erde → Mars (min)', + [BrightChainStrings.DatePage_Telemetry_MarsDelayMax]: 'Erde → Mars (max)', [BrightChainStrings.DatePage_Telemetry_MarsDelayCurrent]: 'Erde → Mars (aktuelle Schätzung)', [BrightChainStrings.DatePage_Telemetry_Unit]: 'md', @@ -2033,4 +2031,23 @@ export const GermanStrings: ComponentStrings = { [BrightChainStrings.JouleEventLog_Col_Kind]: 'Art', [BrightChainStrings.JouleEventLog_Col_Amount]: 'Betrag', [BrightChainStrings.JouleEventLog_Col_Description]: 'Beschreibung', + + // Slogans + [BrightChainStrings.Slogan_Math_Search_Warrant]: + 'Mathematik akzeptiert keinen Durchsuchungsbefehl.', + [BrightChainStrings.Slogan_Signal_Belongs_To_You]: + 'Das Signal gehört dir. Das Rauschen gehört allen.', + [BrightChainStrings.Slogan_Defiance_By_Design]: 'Widerstand durch Design.', + [BrightChainStrings.Slogan_BrightChain_Privacy]: + 'Privatsphäre, um die du nicht bitten musst.', + [BrightChainStrings.Slogan_BrightChain_Privacy_Long]: + 'BrightChain: Privatsphäre, um die du nicht bitten musst.', + [BrightChainStrings.Slogan_Speak_Freely]: + 'Sprich frei; die Mathematik schützt dich.', + [BrightChainStrings.Slogan_Distributed_By_Many]: + 'Von vielen verteilt, von niemandem besessen.', + [BrightChainStrings.Slogan_Truth_In_The_Signal]: + 'Wahrheit im Signal, Sicherheit im Rauschen.', + [BrightChainStrings.Slogan_Ideas_Paper_Trail]: + 'Ideen sollten keine Papierspur hinterlassen.', }; diff --git a/brightchain-lib/src/lib/i18n/strings/japanese.ts b/brightchain-lib/src/lib/i18n/strings/japanese.ts index 7a0be2fc..29fe75f5 100644 --- a/brightchain-lib/src/lib/i18n/strings/japanese.ts +++ b/brightchain-lib/src/lib/i18n/strings/japanese.ts @@ -1850,10 +1850,8 @@ export const JapaneseStrings: ComponentStrings = { [BrightChainStrings.DatePage_Telemetry_Subtitle]: '現在の片道光遅延(ミリデイ単位、md)', [BrightChainStrings.DatePage_Telemetry_MoonDelay]: '地球 → 月', - [BrightChainStrings.DatePage_Telemetry_MarsDelayMin]: - '地球 → 火星(最小)', - [BrightChainStrings.DatePage_Telemetry_MarsDelayMax]: - '地球 → 火星(最大)', + [BrightChainStrings.DatePage_Telemetry_MarsDelayMin]: '地球 → 火星(最小)', + [BrightChainStrings.DatePage_Telemetry_MarsDelayMax]: '地球 → 火星(最大)', [BrightChainStrings.DatePage_Telemetry_MarsDelayCurrent]: '地球 → 火星(現在の推定)', [BrightChainStrings.DatePage_Telemetry_Unit]: 'md', @@ -1914,4 +1912,22 @@ export const JapaneseStrings: ComponentStrings = { [BrightChainStrings.JouleEventLog_Col_Kind]: '種類', [BrightChainStrings.JouleEventLog_Col_Amount]: '金額', [BrightChainStrings.JouleEventLog_Col_Description]: '説明', + + // Slogans + [BrightChainStrings.Slogan_Math_Search_Warrant]: + '数学は捜索令状を受け付けない。', + [BrightChainStrings.Slogan_Signal_Belongs_To_You]: + 'シグナルはあなたのもの。ノイズはみんなのもの。', + [BrightChainStrings.Slogan_Defiance_By_Design]: '設計による反骨。', + [BrightChainStrings.Slogan_BrightChain_Privacy]: + '許可を求めなくていいプライバシー。', + [BrightChainStrings.Slogan_BrightChain_Privacy_Long]: + 'BrightChain:許可を求めなくていいプライバシー。', + [BrightChainStrings.Slogan_Speak_Freely]: '自由に語れ。数学があなたを守る。', + [BrightChainStrings.Slogan_Distributed_By_Many]: + '多くの人が配布し、誰も所有しない。', + [BrightChainStrings.Slogan_Truth_In_The_Signal]: + 'シグナルに真実、ノイズに安全。', + [BrightChainStrings.Slogan_Ideas_Paper_Trail]: + 'アイデアに痕跡を残すべきではない。', }; diff --git a/brightchain-lib/src/lib/i18n/strings/mandarin.ts b/brightchain-lib/src/lib/i18n/strings/mandarin.ts index f267e8e6..7f1379a3 100644 --- a/brightchain-lib/src/lib/i18n/strings/mandarin.ts +++ b/brightchain-lib/src/lib/i18n/strings/mandarin.ts @@ -1781,10 +1781,8 @@ export const MandarinStrings: ComponentStrings = { [BrightChainStrings.DatePage_Telemetry_Subtitle]: '当前单向光延迟,以毫日(md)表示', [BrightChainStrings.DatePage_Telemetry_MoonDelay]: '地球 → 月球', - [BrightChainStrings.DatePage_Telemetry_MarsDelayMin]: - '地球 → 火星(最小)', - [BrightChainStrings.DatePage_Telemetry_MarsDelayMax]: - '地球 → 火星(最大)', + [BrightChainStrings.DatePage_Telemetry_MarsDelayMin]: '地球 → 火星(最小)', + [BrightChainStrings.DatePage_Telemetry_MarsDelayMax]: '地球 → 火星(最大)', [BrightChainStrings.DatePage_Telemetry_MarsDelayCurrent]: '地球 → 火星(当前估计)', [BrightChainStrings.DatePage_Telemetry_Unit]: 'md', @@ -1841,4 +1839,18 @@ export const MandarinStrings: ComponentStrings = { [BrightChainStrings.JouleEventLog_Col_Kind]: '类型', [BrightChainStrings.JouleEventLog_Col_Amount]: '金额', [BrightChainStrings.JouleEventLog_Col_Description]: '描述', + + // Slogans + [BrightChainStrings.Slogan_Math_Search_Warrant]: '数学不接受搜查令。', + [BrightChainStrings.Slogan_Signal_Belongs_To_You]: + '信号属于你。噪声属于所有人。', + [BrightChainStrings.Slogan_Defiance_By_Design]: '以设计之名,坚守反抗。', + [BrightChainStrings.Slogan_BrightChain_Privacy]: '无需申请的隐私保护。', + [BrightChainStrings.Slogan_BrightChain_Privacy_Long]: + 'BrightChain:无需申请的隐私保护。', + [BrightChainStrings.Slogan_Speak_Freely]: '自由发声;数学为你护航。', + [BrightChainStrings.Slogan_Distributed_By_Many]: '众人分布,无人占有。', + [BrightChainStrings.Slogan_Truth_In_The_Signal]: + '信号中有真相,噪声中有安全。', + [BrightChainStrings.Slogan_Ideas_Paper_Trail]: '思想不应留下痕迹。', }; diff --git a/brightchain-lib/src/lib/i18n/strings/spanish.ts b/brightchain-lib/src/lib/i18n/strings/spanish.ts index 41e0b488..6251635d 100644 --- a/brightchain-lib/src/lib/i18n/strings/spanish.ts +++ b/brightchain-lib/src/lib/i18n/strings/spanish.ts @@ -1971,10 +1971,8 @@ export const SpanishStrings: ComponentStrings = { [BrightChainStrings.DatePage_Telemetry_Subtitle]: 'Retardo de luz unidireccional actual expresado en milidías (md)', [BrightChainStrings.DatePage_Telemetry_MoonDelay]: 'Tierra → Luna', - [BrightChainStrings.DatePage_Telemetry_MarsDelayMin]: - 'Tierra → Marte (mín)', - [BrightChainStrings.DatePage_Telemetry_MarsDelayMax]: - 'Tierra → Marte (máx)', + [BrightChainStrings.DatePage_Telemetry_MarsDelayMin]: 'Tierra → Marte (mín)', + [BrightChainStrings.DatePage_Telemetry_MarsDelayMax]: 'Tierra → Marte (máx)', [BrightChainStrings.DatePage_Telemetry_MarsDelayCurrent]: 'Tierra → Marte (est. actual)', [BrightChainStrings.DatePage_Telemetry_Unit]: 'md', @@ -2039,4 +2037,23 @@ export const SpanishStrings: ComponentStrings = { [BrightChainStrings.JouleEventLog_Col_Kind]: 'Tipo', [BrightChainStrings.JouleEventLog_Col_Amount]: 'Monto', [BrightChainStrings.JouleEventLog_Col_Description]: 'Descripción', + + // Slogans + [BrightChainStrings.Slogan_Math_Search_Warrant]: + 'Las matemáticas no aceptan una orden de registro.', + [BrightChainStrings.Slogan_Signal_Belongs_To_You]: + 'La señal te pertenece. El ruido pertenece a todos.', + [BrightChainStrings.Slogan_Defiance_By_Design]: 'Rebeldía por diseño.', + [BrightChainStrings.Slogan_BrightChain_Privacy]: + 'Privacidad que no tienes que pedir.', + [BrightChainStrings.Slogan_BrightChain_Privacy_Long]: + 'BrightChain: Privacidad que no tienes que pedir.', + [BrightChainStrings.Slogan_Speak_Freely]: + 'Habla libremente; las matemáticas te respaldan.', + [BrightChainStrings.Slogan_Distributed_By_Many]: + 'Distribuido por muchos, poseído por nadie.', + [BrightChainStrings.Slogan_Truth_In_The_Signal]: + 'La verdad en la señal, la seguridad en el ruido.', + [BrightChainStrings.Slogan_Ideas_Paper_Trail]: + 'Las ideas no deberían dejar rastro.', }; diff --git a/brightchain-lib/src/lib/i18n/strings/ukrainian.ts b/brightchain-lib/src/lib/i18n/strings/ukrainian.ts index 6ac0f38c..16b9b27e 100644 --- a/brightchain-lib/src/lib/i18n/strings/ukrainian.ts +++ b/brightchain-lib/src/lib/i18n/strings/ukrainian.ts @@ -1951,10 +1951,8 @@ export const UkrainianStrings: ComponentStrings = { [BrightChainStrings.DatePage_Telemetry_Subtitle]: 'Поточна одностороння світлова затримка у мілідобах (md)', [BrightChainStrings.DatePage_Telemetry_MoonDelay]: 'Земля → Місяць', - [BrightChainStrings.DatePage_Telemetry_MarsDelayMin]: - 'Земля → Марс (мін)', - [BrightChainStrings.DatePage_Telemetry_MarsDelayMax]: - 'Земля → Марс (макс)', + [BrightChainStrings.DatePage_Telemetry_MarsDelayMin]: 'Земля → Марс (мін)', + [BrightChainStrings.DatePage_Telemetry_MarsDelayMax]: 'Земля → Марс (макс)', [BrightChainStrings.DatePage_Telemetry_MarsDelayCurrent]: 'Земля → Марс (поточна оцінка)', [BrightChainStrings.DatePage_Telemetry_Unit]: 'md', @@ -2015,4 +2013,23 @@ export const UkrainianStrings: ComponentStrings = { [BrightChainStrings.JouleEventLog_Col_Kind]: 'Тип', [BrightChainStrings.JouleEventLog_Col_Amount]: 'Сума', [BrightChainStrings.JouleEventLog_Col_Description]: 'Опис', + + // Slogans + [BrightChainStrings.Slogan_Math_Search_Warrant]: + 'Математика не приймає ордер на обшук.', + [BrightChainStrings.Slogan_Signal_Belongs_To_You]: + 'Сигнал належить тобі. Шум належить усім.', + [BrightChainStrings.Slogan_Defiance_By_Design]: 'Спротив через дизайн.', + [BrightChainStrings.Slogan_BrightChain_Privacy]: + 'Приватність, про яку не треба просити.', + [BrightChainStrings.Slogan_BrightChain_Privacy_Long]: + 'BrightChain: Приватність, про яку не треба просити.', + [BrightChainStrings.Slogan_Speak_Freely]: + 'Говори вільно; математика тебе захищає.', + [BrightChainStrings.Slogan_Distributed_By_Many]: + 'Розподілено багатьма, не належить нікому.', + [BrightChainStrings.Slogan_Truth_In_The_Signal]: + 'Правда в сигналі, безпека в шумі.', + [BrightChainStrings.Slogan_Ideas_Paper_Trail]: + 'Ідеї не повинні залишати паперового сліду.', }; diff --git a/brightchain-lib/src/lib/interfaces/branded/serializers/serializers.spec.ts b/brightchain-lib/src/lib/interfaces/branded/serializers/serializers.spec.ts index 985eefd4..e1eed49c 100644 --- a/brightchain-lib/src/lib/interfaces/branded/serializers/serializers.spec.ts +++ b/brightchain-lib/src/lib/interfaces/branded/serializers/serializers.spec.ts @@ -45,7 +45,7 @@ describe('deserialize() with invalid JSON returns INVALID_JSON error code (Req 5 for (const bad of invalidJsonCases) { const result = nodeStatusSerializer.deserialize(bad); expect(result.success).toBe(false); - if (!result.success) { + if (result.success === false) { expect(result.error.code).toBe('INVALID_JSON'); } } @@ -55,7 +55,7 @@ describe('deserialize() with invalid JSON returns INVALID_JSON error code (Req 5 for (const bad of invalidJsonCases) { const result = peerInfoSerializer.deserialize(bad); expect(result.success).toBe(false); - if (!result.success) { + if (result.success === false) { expect(result.error.code).toBe('INVALID_JSON'); } } @@ -65,7 +65,7 @@ describe('deserialize() with invalid JSON returns INVALID_JSON error code (Req 5 for (const bad of invalidJsonCases) { const result = poolInfoSerializer.deserialize(bad); expect(result.success).toBe(false); - if (!result.success) { + if (result.success === false) { expect(result.error.code).toBe('INVALID_JSON'); } } @@ -75,7 +75,7 @@ describe('deserialize() with invalid JSON returns INVALID_JSON error code (Req 5 for (const bad of invalidJsonCases) { const result = energyAccountStatusSerializer.deserialize(bad); expect(result.success).toBe(false); - if (!result.success) { + if (result.success === false) { expect(result.error.code).toBe('INVALID_JSON'); } } @@ -91,7 +91,7 @@ describe('deserialize() with schema-mismatched JSON returns VALIDATION_FAILED er it('nodeStatusSerializer: returns VALIDATION_FAILED for empty object JSON', () => { const result = nodeStatusSerializer.deserialize('{}'); expect(result.success).toBe(false); - if (!result.success) { + if (result.success === false) { expect(result.error.code).toBe('VALIDATION_FAILED'); } }); @@ -100,7 +100,7 @@ describe('deserialize() with schema-mismatched JSON returns VALIDATION_FAILED er const json = JSON.stringify({ nodeId: 'abc', healthy: true }); const result = nodeStatusSerializer.deserialize(json); expect(result.success).toBe(false); - if (!result.success) { + if (result.success === false) { expect(result.error.code).toBe('VALIDATION_FAILED'); } }); @@ -116,7 +116,7 @@ describe('deserialize() with schema-mismatched JSON returns VALIDATION_FAILED er }); const result = nodeStatusSerializer.deserialize(json); expect(result.success).toBe(false); - if (!result.success) { + if (result.success === false) { expect(result.error.code).toBe('VALIDATION_FAILED'); } }); @@ -124,7 +124,7 @@ describe('deserialize() with schema-mismatched JSON returns VALIDATION_FAILED er it('peerInfoSerializer: returns VALIDATION_FAILED for empty object JSON', () => { const result = peerInfoSerializer.deserialize('{}'); expect(result.success).toBe(false); - if (!result.success) { + if (result.success === false) { expect(result.error.code).toBe('VALIDATION_FAILED'); } }); @@ -137,7 +137,7 @@ describe('deserialize() with schema-mismatched JSON returns VALIDATION_FAILED er }); const result = peerInfoSerializer.deserialize(json); expect(result.success).toBe(false); - if (!result.success) { + if (result.success === false) { expect(result.error.code).toBe('VALIDATION_FAILED'); } }); @@ -155,7 +155,7 @@ describe('deserialize() with schema-mismatched JSON returns VALIDATION_FAILED er }); const result = poolInfoSerializer.deserialize(json); expect(result.success).toBe(false); - if (!result.success) { + if (result.success === false) { expect(result.error.code).toBe('VALIDATION_FAILED'); } }); @@ -163,7 +163,7 @@ describe('deserialize() with schema-mismatched JSON returns VALIDATION_FAILED er it('energyAccountStatusSerializer: returns VALIDATION_FAILED for empty object JSON', () => { const result = energyAccountStatusSerializer.deserialize('{}'); expect(result.success).toBe(false); - if (!result.success) { + if (result.success === false) { expect(result.error.code).toBe('VALIDATION_FAILED'); } }); @@ -178,7 +178,7 @@ describe('deserialize() with schema-mismatched JSON returns VALIDATION_FAILED er }); const result = blockStoreStatsSerializer.deserialize(json); expect(result.success).toBe(false); - if (!result.success) { + if (result.success === false) { expect(result.error.code).toBe('VALIDATION_FAILED'); } }); @@ -192,7 +192,7 @@ describe('deserialize() with schema-mismatched JSON returns VALIDATION_FAILED er }); const result = clientEventSerializer.deserialize(json); expect(result.success).toBe(false); - if (!result.success) { + if (result.success === false) { expect(result.error.code).toBe('VALIDATION_FAILED'); } }); @@ -200,7 +200,7 @@ describe('deserialize() with schema-mismatched JSON returns VALIDATION_FAILED er it('nodeStatusSerializer: returns VALIDATION_FAILED for a JSON array', () => { const result = nodeStatusSerializer.deserialize('[1, 2, 3]'); expect(result.success).toBe(false); - if (!result.success) { + if (result.success === false) { expect(result.error.code).toBe('VALIDATION_FAILED'); } }); @@ -208,7 +208,7 @@ describe('deserialize() with schema-mismatched JSON returns VALIDATION_FAILED er it('nodeStatusSerializer: returns VALIDATION_FAILED for a JSON null', () => { const result = nodeStatusSerializer.deserialize('null'); expect(result.success).toBe(false); - if (!result.success) { + if (result.success === false) { expect(result.error.code).toBe('VALIDATION_FAILED'); } }); diff --git a/brightchain-lib/src/lib/utils/dateUtils.property.spec.ts b/brightchain-lib/src/lib/utils/dateUtils.property.spec.ts index 9e342177..9a595d7a 100644 --- a/brightchain-lib/src/lib/utils/dateUtils.property.spec.ts +++ b/brightchain-lib/src/lib/utils/dateUtils.property.spec.ts @@ -296,7 +296,7 @@ describe('Feature: block-security-hardening, Property 8: Malformed Date Rejectio fc.array(fc.anything()), ), (nonDate) => { - // @ts-expect-error - Testing runtime behavior with invalid types + // @ts-ignore - Testing runtime behavior with invalid types expect(isValidDate(nonDate)).toBe(false); }, ), @@ -333,7 +333,7 @@ describe('Feature: block-security-hardening, Property 8: Malformed Date Rejectio fc.object(), ), (nonDate) => { - // @ts-expect-error - Testing runtime behavior with invalid types + // @ts-ignore - Testing runtime behavior with invalid types expect(() => serializeDate(nonDate)).toThrow(/Invalid date object/i); }, ), diff --git a/brightchain-lib/tsconfig.spec.json b/brightchain-lib/tsconfig.spec.json index 7a3f0924..36479dc6 100644 --- a/brightchain-lib/tsconfig.spec.json +++ b/brightchain-lib/tsconfig.spec.json @@ -15,6 +15,9 @@ "noImplicitThis": true, "alwaysStrict": true }, + "files": [ + "src/test/types/jest.d.ts" + ], "include": [ "jest.config.ts", "src/**/*.test.ts", diff --git a/brightchain-node-express-suite/jest.config.cts b/brightchain-node-express-suite/jest.config.cts index 0843dc3e..052c9987 100644 --- a/brightchain-node-express-suite/jest.config.cts +++ b/brightchain-node-express-suite/jest.config.cts @@ -6,7 +6,7 @@ module.exports = { '^.+\\.[tj]s$': [ 'ts-jest', { - tsconfig: '/tsconfig.spec.json', + tsconfig: require('path').join(__dirname, 'tsconfig.spec.json'), diagnostics: false, }, ], diff --git a/brightchain-node-express-suite/package.json b/brightchain-node-express-suite/package.json index 74ec2f02..efc7c20c 100644 --- a/brightchain-node-express-suite/package.json +++ b/brightchain-node-express-suite/package.json @@ -1,6 +1,6 @@ { "name": "@brightchain/node-express-suite", - "version": "0.32.0", + "version": "0.32.1", "type": "commonjs", "main": "./src/index.js", "types": "./src/index.d.ts", @@ -14,8 +14,8 @@ "tslib": "^2.3.0" }, "peerDependencies": { - "@brightchain/brightchain-lib": "^0.32.0", - "@brightchain/db": "^0.32.0", + "@brightchain/brightchain-lib": "^0.32.1", + "@brightchain/db": "^0.32.1", "@digitaldefiance/branded-interface": "0.0.5", "@digitaldefiance/node-express-suite": "4.28.3", "bcrypt": "^5.0.0", diff --git a/brightchain-node-express-suite/src/lib/middleware/validateBody.ts b/brightchain-node-express-suite/src/lib/middleware/validateBody.ts index a4f36847..a9013f2d 100644 --- a/brightchain-node-express-suite/src/lib/middleware/validateBody.ts +++ b/brightchain-node-express-suite/src/lib/middleware/validateBody.ts @@ -24,7 +24,7 @@ export function validateBody>( return; } const result = safeParseInterface(req.body, definition); - if (!result.success) { + if (result.success === false) { res.status(400).json({ error: result.error.message, code: result.error.code, diff --git a/brightchain-react-components/jest.config.cts b/brightchain-react-components/jest.config.cts index 9cf8e40e..1dc108ca 100644 --- a/brightchain-react-components/jest.config.cts +++ b/brightchain-react-components/jest.config.cts @@ -4,7 +4,7 @@ module.exports = { testEnvironment: 'jsdom', setupFiles: ['/src/test-setup.ts'], transform: { - '^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + '^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: require('path').join(__dirname, 'tsconfig.spec.json') }], }, moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], coverageDirectory: '../coverage/brightchain-react-components', diff --git a/brightchain-react-components/package.json b/brightchain-react-components/package.json index 891c2e63..2697d768 100644 --- a/brightchain-react-components/package.json +++ b/brightchain-react-components/package.json @@ -1,12 +1,12 @@ { "name": "@brightchain/brightchain-react-components", - "version": "0.32.0", + "version": "0.32.1", "type": "commonjs", "main": "./src/index.js", "browser": "./src/browser.ts", "types": "./src/index.d.ts", "dependencies": { - "@brightchain/brightchain-lib": "^0.32.0", + "@brightchain/brightchain-lib": "^0.32.1", "@digitaldefiance/ecies-lib": "5.2.2", "@digitaldefiance/express-suite-react-components": "4.29.1", "@digitaldefiance/i18n-lib": "4.7.5", @@ -15,6 +15,7 @@ "@mui/icons-material": "^7.3.7", "@mui/material": "^7.3.7", "@mui/utils": "7.3.9", + "framer-motion": "^12.38.0", "react": "19.2.4", "react-router-dom": "6.11.2", "tslib": "^2.3.0" diff --git a/brightchain-react-components/src/browser.ts b/brightchain-react-components/src/browser.ts index 26eeee81..8ec85fd2 100644 --- a/brightchain-react-components/src/browser.ts +++ b/brightchain-react-components/src/browser.ts @@ -9,6 +9,7 @@ export * from './lib/BrightChainLogo'; export * from './lib/BrightChainLogoI18N'; // BrightChainSoupDemo intentionally omitted — depends on @digitaldefiance/ecies-lib (Node.js crypto) export * from './lib/BrightChainSubLogo'; +export * from './lib/BrightDate'; export * from './lib/communication'; export * from './lib/identity'; export * from './lib/layout'; diff --git a/brightchain-react-components/src/lib/BrightChainLogo.scss b/brightchain-react-components/src/lib/BrightChainLogo.scss index 2b3c369b..1cec546f 100644 --- a/brightchain-react-components/src/lib/BrightChainLogo.scss +++ b/brightchain-react-components/src/lib/BrightChainLogo.scss @@ -3,7 +3,7 @@ src: url('/assets/fonts/Inter-Bold.woff2') format('woff2'); font-weight: 700; font-style: normal; - font-display: swap; + font-display: block; } @font-face { @@ -11,5 +11,13 @@ src: url('/assets/fonts/IBMPlexSans-Regular.woff2') format('woff2'); font-weight: 400; font-style: normal; - font-display: swap; + font-display: block; +} + +@font-face { + font-family: 'IBMPlexSans'; + src: url('/assets/fonts/IBMPlexSans-SemiBold.woff2') format('woff2'); + font-weight: 600; + font-style: normal; + font-display: block; } diff --git a/brightchain-react-components/src/lib/BrightChainLogo.tsx b/brightchain-react-components/src/lib/BrightChainLogo.tsx index 78c63eed..c914edef 100644 --- a/brightchain-react-components/src/lib/BrightChainLogo.tsx +++ b/brightchain-react-components/src/lib/BrightChainLogo.tsx @@ -1,5 +1,5 @@ import { CONSTANTS } from '@brightchain/brightchain-lib'; -import React from 'react'; +import React, { useEffect, useId, useRef, useState } from 'react'; import './BrightChainLogo.scss'; export interface BrightChainLogoProps { @@ -10,12 +10,24 @@ export interface BrightChainLogoProps { secondaryText?: string; taglineColor?: string; taglineText?: string; + /** Accessible label for the logo. Defaults to "{primaryText}{secondaryText}". */ + ariaLabel?: string; className?: string; style?: React.CSSProperties; width?: number | string; height?: number | string; } +/** Fixed height of the SVG coordinate space. */ +const VIEWBOX_HEIGHT = 600; +/** + * Initial viewBox width — generous enough for English with the correct fonts. + * Will be replaced after fonts load and getBBox() can measure accurately. + */ +const VIEWBOX_WIDTH_DEFAULT = 3200; +/** Right padding in SVG units added after the widest text element. */ +const VIEWBOX_RIGHT_PAD = 40; + export const BrightChainLogo: React.FC = ({ primaryColor: brightColor = CONSTANTS.THEME_COLORS.CHAIN_BLUE_DARK, primaryText = 'Bright', @@ -24,19 +36,62 @@ export const BrightChainLogo: React.FC = ({ secondaryText = 'Chain', taglineColor = '#ffffff', taglineText = 'Privacy. Participation. Power.', + ariaLabel, className, style, - width = 3151, - height = 649, -}) => ( + width, + height = 600, +}) => { + const svgRef = useRef(null); + const [viewBoxWidth, setViewBoxWidth] = useState(VIEWBOX_WIDTH_DEFAULT); + const titleId = useId(); + + useEffect(() => { + const svg = svgRef.current; + if (!svg) return; + + const measure = () => { + try { + // Measure the full content bounding box of the SVG in user-unit space. + // This includes the logo mark paths + all text, so it's the true width. + const bbox = svg.getBBox(); + if (bbox.width > 0) { + setViewBoxWidth(bbox.x + bbox.width + VIEWBOX_RIGHT_PAD); + } + } catch { + // SSR or detached element — keep default + } + }; + + // Wait for fonts before measuring so getBBox() uses the real typeface. + // After fonts load, also wait one rAF so the browser has laid out the SVG. + if (typeof document !== 'undefined' && document.fonts?.ready) { + document.fonts.ready.then(() => { + requestAnimationFrame(() => requestAnimationFrame(measure)); + }); + } else { + requestAnimationFrame(() => requestAnimationFrame(measure)); + } + }, [primaryText, secondaryText, taglineText]); + + const resolvedHeight = + height ?? (typeof width === 'number' ? Math.round(width / (viewBoxWidth / VIEWBOX_HEIGHT)) : undefined); + + const resolvedAriaLabel = ariaLabel ?? `${primaryText}${secondaryText} ${taglineText}`; + + return ( + {resolvedAriaLabel} = ({ - + {taglineText} -); + ); +}; export default BrightChainLogo; diff --git a/brightchain-react-components/src/lib/BrightChainLogoI18N.tsx b/brightchain-react-components/src/lib/BrightChainLogoI18N.tsx index 603701d1..77b8d9f0 100644 --- a/brightchain-react-components/src/lib/BrightChainLogoI18N.tsx +++ b/brightchain-react-components/src/lib/BrightChainLogoI18N.tsx @@ -8,6 +8,7 @@ export interface BrightChainLogoI18NProps { primarySecondaryBreak?: boolean; secondaryColor?: string; taglineColor?: string; + taglineText?: string; className?: string; style?: React.CSSProperties; width?: number | string; @@ -19,6 +20,7 @@ export const BrightChainLogoI18N: React.FC = ({ primarySecondaryBreak = false, secondaryColor: chainColor = CONSTANTS.THEME_COLORS.CHAIN_BLUE_LIGHT, taglineColor = '#ffffff', + taglineText, className, style, width = 3151, @@ -143,7 +145,7 @@ export const BrightChainLogoI18N: React.FC = ({ secondaryColor: chainColor, secondaryText: secondaryText, taglineColor, - taglineText: t( + taglineText: taglineText ?? t( BrightChainStrings.Common_PrivacyParticipationPower, undefined, languageCode, diff --git a/brightchain-react-components/src/lib/BrightChainLogoI18NRotation.tsx b/brightchain-react-components/src/lib/BrightChainLogoI18NRotation.tsx new file mode 100644 index 00000000..23a2708f --- /dev/null +++ b/brightchain-react-components/src/lib/BrightChainLogoI18NRotation.tsx @@ -0,0 +1,64 @@ +import { FC, useEffect, useMemo, useState } from 'react'; +import { useI18n } from '@digitaldefiance/express-suite-react-components'; +import { BrightChainStrings, CONSTANTS } from '@brightchain/brightchain-lib'; +import { BrightChainLogoI18N } from './BrightChainLogoI18N'; + +export interface BrightChainLogoI18NRotationProps { + primaryColor?: string; + primarySecondaryBreak?: boolean; + secondaryColor?: string; + taglineColor?: string; + className?: string; + style?: React.CSSProperties; + width?: number | string; + height?: number | string; +} + +export const BrightChainLogoI18NRotation: FC = ({ + primaryColor: brightColor = CONSTANTS.THEME_COLORS.CHAIN_BLUE_DARK, + primarySecondaryBreak = false, + secondaryColor: chainColor = CONSTANTS.THEME_COLORS.CHAIN_BLUE_LIGHT, + taglineColor = '#ffffff', + className, + style, + width = 3151, + height = 649, +}) => { + const slogans = useMemo( + () => [ + BrightChainStrings.Slogan_Math_Search_Warrant, + BrightChainStrings.Slogan_Signal_Belongs_To_You, + BrightChainStrings.Slogan_Defiance_By_Design, + BrightChainStrings.Slogan_BrightChain_Privacy, + BrightChainStrings.Slogan_Speak_Freely, + BrightChainStrings.Slogan_Distributed_By_Many, + BrightChainStrings.Slogan_Truth_In_The_Signal, + BrightChainStrings.Slogan_Ideas_Paper_Trail, + ], + [], + ); + const { tBranded: t } = useI18n(); + const [currentIndex, setCurrentIndex] = useState(0); + + useEffect(() => { + const intervalId = setInterval(() => { + setCurrentIndex((prev) => (prev + 1) % slogans.length); + }, 5000); + + return () => clearInterval(intervalId); + }, [slogans.length]); + + return ( + + ); +}; \ No newline at end of file diff --git a/brightchain-react-components/src/lib/SloganRotation.scss b/brightchain-react-components/src/lib/SloganRotation.scss new file mode 100644 index 00000000..b0dd9563 --- /dev/null +++ b/brightchain-react-components/src/lib/SloganRotation.scss @@ -0,0 +1,22 @@ +@use './brightchain'; + +.hero-badge-slogan { + display: inline-block; + padding: 0.5rem 1.5rem; + background: transparent; + border: 2px solid var(--accent-secondary, #0a60d0); + border-radius: 20px; + margin-bottom: 2rem; +} + +.badge-slogan { + color: var(--accent-secondary, #0a60d0); + font-weight: 600; + font-size: 0.95rem; +} + +.badge-text-slogan { + color: var(--accent-secondary, #0a60d0); + font-weight: 600; + font-size: 0.95rem; +} \ No newline at end of file diff --git a/brightchain-react-components/src/lib/SloganRotation.tsx b/brightchain-react-components/src/lib/SloganRotation.tsx new file mode 100644 index 00000000..c294d008 --- /dev/null +++ b/brightchain-react-components/src/lib/SloganRotation.tsx @@ -0,0 +1,42 @@ +import { motion } from 'framer-motion'; +import { FC, useEffect, useMemo, useState } from 'react'; +import './SloganRotation.scss'; +import { useI18n } from '@digitaldefiance/express-suite-react-components'; +import { BrightChainStrings } from '@brightchain/brightchain-lib'; + +export const SloganRotation: FC = () => { + const slogans = useMemo( + () => [ + BrightChainStrings.Slogan_Math_Search_Warrant, + BrightChainStrings.Slogan_Signal_Belongs_To_You, + BrightChainStrings.Slogan_Defiance_By_Design, + BrightChainStrings.Slogan_BrightChain_Privacy, + BrightChainStrings.Slogan_Speak_Freely, + BrightChainStrings.Slogan_Distributed_By_Many, + BrightChainStrings.Slogan_Truth_In_The_Signal, + BrightChainStrings.Slogan_Ideas_Paper_Trail, + ], + [], + ); + const { tBranded: t } = useI18n(); + const [currentIndex, setCurrentIndex] = useState(0); + + useEffect(() => { + const intervalId = setInterval(() => { + setCurrentIndex((prev) => (prev + 1) % slogans.length); + }, 5000); + + return () => clearInterval(intervalId); + }, [slogans.length]); + + return ( + + {t(slogans[currentIndex])} + + ); +}; \ No newline at end of file diff --git a/brightchain-react-components/src/lib/__tests__/AppSidebar.property.spec.tsx b/brightchain-react-components/src/lib/__tests__/AppSidebar.property.spec.tsx index 041b7e82..ee727b9b 100644 --- a/brightchain-react-components/src/lib/__tests__/AppSidebar.property.spec.tsx +++ b/brightchain-react-components/src/lib/__tests__/AppSidebar.property.spec.tsx @@ -4,6 +4,10 @@ * Uses fast-check to verify universal correctness properties * across randomly generated inputs. */ +// Async React property tests can exceed 5 s under parallel load — give them room. +jest.retryTimes(2, { logErrorsBeforeRetry: false }); +jest.setTimeout(30000); + import '@testing-library/jest-dom'; import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import fc from 'fast-check'; diff --git a/brightchain-react-components/src/lib/__tests__/LayoutShell.property.spec.tsx b/brightchain-react-components/src/lib/__tests__/LayoutShell.property.spec.tsx index 47329705..dcdccea6 100644 --- a/brightchain-react-components/src/lib/__tests__/LayoutShell.property.spec.tsx +++ b/brightchain-react-components/src/lib/__tests__/LayoutShell.property.spec.tsx @@ -4,6 +4,10 @@ * Uses fast-check to verify universal correctness properties * across randomly generated inputs. */ +// Async React property tests can exceed 5 s under parallel load — give them room. +jest.retryTimes(2, { logErrorsBeforeRetry: false }); +jest.setTimeout(30000); + import { useTheme } from '@mui/material/styles'; import '@testing-library/jest-dom'; import { cleanup, render, screen } from '@testing-library/react'; diff --git a/brightchain-react-components/src/lib/brightchain.scss b/brightchain-react-components/src/lib/brightchain.scss new file mode 100644 index 00000000..3ade2715 --- /dev/null +++ b/brightchain-react-components/src/lib/brightchain.scss @@ -0,0 +1,29 @@ +:root { + /* BrightChain Blue Theme */ + --bg-primary: #0a0e1a; + --bg-secondary: #111827; + --bg-tertiary: #1f2937; + --text-primary: #ffffff; + --text-secondary: #9ca3af; + --accent-primary: #46b6fd; /* BrightChain blue */ + --accent-secondary: #0a60d0; /* Deep blue */ + --accent-tertiary: #5ecbff; /* Lighter blue for highlights */ + --gradient-primary: linear-gradient(135deg, #2daaff, #0074e0); + --chain-blue: #1976d2; + --chain-blue-light: #46b6fd; + --chain-blue-dark: #0a60d0; + --tagline-light: #ffffff; + --tagline-dark: #000000; + --bright-cyan: #00bcd4; + --bright-cyan-light: #4dd0e1; + --bright-cyan-dark: #0097a7; + --crypto-purple: #9c27b0; + --secure-green: #4caf50; + --alert-orange: #ff9800; + --error-red: #f44336; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} \ No newline at end of file diff --git a/brightchain-react-components/src/lib/index.ts b/brightchain-react-components/src/lib/index.ts index 9d3e97b3..15a5887a 100644 --- a/brightchain-react-components/src/lib/index.ts +++ b/brightchain-react-components/src/lib/index.ts @@ -1,5 +1,6 @@ export * from './BrightChainLogo'; export * from './BrightChainLogoI18N'; +export * from './BrightChainLogoI18NRotation'; export * from './BrightChainSoupDemo'; export * from './BrightChainSubLogo'; export * from './BrightDate'; @@ -14,4 +15,5 @@ export * from './joule'; export * from './layout'; export * from './settings/BrightDateDisplaySelector'; export * from './showcase'; -export * from './SimpleSoupDemo'; \ No newline at end of file +export * from './SimpleSoupDemo'; +export * from './SloganRotation'; \ No newline at end of file diff --git a/brightchain-react-e2e/playwright.config.monitored.ts b/brightchain-react-e2e/playwright.config.monitored.ts new file mode 100644 index 00000000..e91124f3 --- /dev/null +++ b/brightchain-react-e2e/playwright.config.monitored.ts @@ -0,0 +1,82 @@ +import { nxE2EPreset } from '@nx/playwright/preset'; +import { defineConfig, devices } from '@playwright/test'; +import { workspaceRoot } from '@nx/devkit'; +import path from 'path'; + +// The API server serves both REST endpoints and the React SPA on port 3000. +const baseURL = process.env['BASE_URL'] || 'http://localhost:3000'; + +export default defineConfig({ + ...nxE2EPreset(__filename, { testDir: './src' }), + globalSetup: path.join(workspaceRoot, 'brightchain-react-e2e/src/global-setup.ts'), + /* Generous timeout — lazy chunks + API calls + rendering + server load */ + timeout: 120_000, + /* Retry flaky tests caused by transient server overload */ + retries: 1, + /* Limit parallelism to reduce server load — lazy chunks fail under heavy concurrency */ + workers: 1, + fullyParallel: false, + /* Global expect timeout — the 60MB SPA bundle needs time to parse, execute, + and render. The default 5s is too short for initial page loads. */ + expect: { + timeout: 30_000, + }, + use: { + baseURL, + trace: 'on-first-retry', + /* Capture screenshot on failure to help debug rendering issues */ + screenshot: 'only-on-failure', + }, + reporter: [ + ['html'], + [path.join(workspaceRoot, 'tools/custom-playwright-reporter.js')] + ], + /* Start the built API server directly (builds are handled by the e2e + target's dependsOn in project.json, so we skip Nx executor overhead). */ + webServer: { + command: 'node --import tsx --no-warnings dist/brightchain-api/main.js', + env: { + /* Prevent the dev .env file from overriding env vars set here. */ + BRIGHTCHAIN_ENV_FILE: '/dev/null', + NODE_ENV: 'development', + NODE_OPTIONS: '--max-old-space-size=16384', + DEV_DATABASE: 'e2e-react-pool', + HOST: 'localhost', + PORT: '3000', + SERVER_URL: 'http://localhost:3000', + DEBUG: 'false', + LETS_ENCRYPT_ENABLED: 'false', + UPNP_ENABLED: 'false', + USE_TRANSACTIONS: 'false', + DISABLE_EMAIL_SEND: 'true', + EMAIL_DOMAIN: 'brightchain.org', + ENABLED_FEATURES: + 'BrightChat,BrightHub,BrightMail,BrightPass,DigitalBurnbag,BrightChart', + JWT_SECRET: + 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2', + MNEMONIC_HMAC_SECRET: + '809ec1ae1ded0d2e89fabec3f9de33d26e3bb56871c3de91b035fd4671b90795', + MNEMONIC_ENCRYPTION_KEY: + '0fddee346b813a97483ee05939edba64dfb7dced822b42b9059f890b86ab496d', + SYSTEM_MNEMONIC: + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', + REACT_DIST_DIR: `${workspaceRoot}/dist/brightchain-react`, + API_DIST_DIR: `${workspaceRoot}/dist/brightchain-api`, + }, + url: 'http://localhost:3000/api/health', + reuseExistingServer: !process.env.CI, + cwd: workspaceRoot, + /* Server init includes crypto setup, DB seeding, and FEC initialization. + When running after other e2e suites, the machine may be under heavy load + so we allow up to 10 minutes for startup. */ + timeout: 600_000, + stdout: 'pipe', + stderr: 'pipe', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); \ No newline at end of file diff --git a/brightchain-react-e2e/playwright.config.ts b/brightchain-react-e2e/playwright.config.ts index d8a60360..1ec570bb 100644 --- a/brightchain-react-e2e/playwright.config.ts +++ b/brightchain-react-e2e/playwright.config.ts @@ -75,4 +75,4 @@ export default defineConfig({ use: { ...devices['Desktop Chrome'] }, }, ], -}); +}); \ No newline at end of file diff --git a/brightchain-react-e2e/src/.auth/shared-auth.json b/brightchain-react-e2e/src/.auth/shared-auth.json index 32cfdb4f..48b942d7 100644 --- a/brightchain-react-e2e/src/.auth/shared-auth.json +++ b/brightchain-react-e2e/src/.auth/shared-auth.json @@ -1 +1 @@ -{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZW1iZXJJZCI6IjU0MmZiYzQ3ZjBlYzQ4MmZiYWRmN2JkMTJkN2JiOTYyIiwidXNlcm5hbWUiOiJlMmVfc2hhcmVkX21vdGJxYzZ3NXRqNXU3IiwidHlwZSI6IlVzZXIiLCJpYXQiOjE3NzgwMjc2MTIsImV4cCI6MTc3ODYzMjQxMn0.2GP_QTIJjY9VFB4umhvJt_ewoxioNSuWAvkZD70eNAg","memberId":"542fbc47f0ec482fbadf7bd12d7bb962","username":"e2e_shared_motbqc6w5tj5u7","email":"e2e_shared_motbqc6w5tj5u7@test.brightchain.local","password":"T3st!Passmotbqc6w5tj5u7"} \ No newline at end of file +{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZW1iZXJJZCI6ImU4YWIwZGFlZWJjNTQ4OTY4ZGYwMmZmNTY5NmNlY2VlIiwidXNlcm5hbWUiOiJlMmVfc2hhcmVkX21veTd1N2J5NDVsODF2IiwidHlwZSI6IlVzZXIiLCJpYXQiOjE3NzgzMjM0MDUsImV4cCI6MTc3ODkyODIwNX0.VVoYvyE_XpZ6KSZG1XDdghlfB54S_Od5oDgsAB1nXaU","memberId":"e8ab0daeebc548968df02ff5696cecee","username":"e2e_shared_moy7u7by45l81v","email":"e2e_shared_moy7u7by45l81v@test.brightchain.local","password":"T3st!Passmoy7u7by45l81v"} \ No newline at end of file diff --git a/brightchain-react/src/app/app.spec.tsx b/brightchain-react/src/app/app.spec.tsx index 9a13b458..9f46f9bb 100644 --- a/brightchain-react/src/app/app.spec.tsx +++ b/brightchain-react/src/app/app.spec.tsx @@ -58,6 +58,9 @@ jest.mock('@brightchain/brightchain-react-components', () => ({ StoragePoolsDemo: () => (
StoragePoolsDemo Mock
), + SloganRotation: () => ( +
SloganRotation Mock
+ ), })); // Mock brighthub-react-components to avoid real useI18n calls diff --git a/brightchain-react/src/app/brightpass-routes.bugfix.property.spec.tsx b/brightchain-react/src/app/brightpass-routes.bugfix.property.spec.tsx index 96f8ec91..962730b9 100644 --- a/brightchain-react/src/app/brightpass-routes.bugfix.property.spec.tsx +++ b/brightchain-react/src/app/brightpass-routes.bugfix.property.spec.tsx @@ -10,6 +10,10 @@ * **Validates: Requirements 1.3** */ +// Async React property tests can exceed 5 s under parallel load — give them room. +jest.retryTimes(2, { logErrorsBeforeRetry: false }); +jest.setTimeout(30000); + import { render } from '@testing-library/react'; import React from 'react'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; diff --git a/brightchain-react/src/app/brightpass-routes.preservation.property.spec.tsx b/brightchain-react/src/app/brightpass-routes.preservation.property.spec.tsx index 2c7576ff..22a6d88d 100644 --- a/brightchain-react/src/app/brightpass-routes.preservation.property.spec.tsx +++ b/brightchain-react/src/app/brightpass-routes.preservation.property.spec.tsx @@ -10,6 +10,10 @@ * **Validates: Requirements 3.3, 3.4, 3.5** */ +// Async React property tests can exceed 5 s under parallel load — give them room. +jest.retryTimes(2, { logErrorsBeforeRetry: false }); +jest.setTimeout(30000); + import { render } from '@testing-library/react'; import fc from 'fast-check'; import React from 'react'; diff --git a/brightchain-react/src/app/components/SplashPage.tsx b/brightchain-react/src/app/components/SplashPage.tsx index 1e6dc050..2bef5bd1 100644 --- a/brightchain-react/src/app/components/SplashPage.tsx +++ b/brightchain-react/src/app/components/SplashPage.tsx @@ -8,6 +8,7 @@ import { BrightChainStrings, THEME_COLORS } from '@brightchain/brightchain-lib'; import { BrightChainLogoI18N, BrightChainSubLogo, + SloganRotation, } from '@brightchain/brightchain-react-components'; import { useI18n } from '@digitaldefiance/express-suite-react-components'; import { @@ -129,6 +130,8 @@ export const SplashPage: FC = () => { /> + + /tsconfig.spec.json', + tsconfig: './tsconfig.spec.json', useESM: true, diagnostics: false, }, diff --git a/brightchain-s3-store/package.json b/brightchain-s3-store/package.json index e89f3486..c1b5acdf 100644 --- a/brightchain-s3-store/package.json +++ b/brightchain-s3-store/package.json @@ -1,6 +1,6 @@ { "name": "@brightchain/s3-store", - "version": "0.32.0", + "version": "0.32.1", "main": "./src/index.js", "types": "./src/index.d.ts", "files": [ @@ -9,8 +9,8 @@ ], "dependencies": { "@aws-sdk/client-s3": "^3.700.0", - "@brightchain/brightchain-api-lib": "^0.32.0", - "@brightchain/brightchain-lib": "^0.32.0", + "@brightchain/brightchain-api-lib": "^0.32.1", + "@brightchain/brightchain-lib": "^0.32.1", "tslib": "^2.3.0" }, "scripts": { diff --git a/brightchain-s3-store/tsconfig.lib.json b/brightchain-s3-store/tsconfig.lib.json index 27e81146..761ab44d 100644 --- a/brightchain-s3-store/tsconfig.lib.json +++ b/brightchain-s3-store/tsconfig.lib.json @@ -14,9 +14,11 @@ "include": ["src/**/*.ts"], "exclude": [ "jest.config.ts", + "jest.config.cts", "src/**/*.spec.ts", "src/**/*.test.ts", - "src/test-setup.ts" + "src/test-setup.ts", + "src/**/__tests__/**" ], "references": [ { diff --git a/brightchain-test-utils/jest.config.ts b/brightchain-test-utils/jest.config.cts similarity index 75% rename from brightchain-test-utils/jest.config.ts rename to brightchain-test-utils/jest.config.cts index 9c778c8d..c0577e1a 100644 --- a/brightchain-test-utils/jest.config.ts +++ b/brightchain-test-utils/jest.config.cts @@ -3,7 +3,7 @@ module.exports = { preset: '../jest.preset.js', testEnvironment: 'node', transform: { - '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: './tsconfig.spec.json' }], }, moduleFileExtensions: ['ts', 'js', 'html'], coverageDirectory: '../coverage/brightchain-test-utils', diff --git a/brightchart-lib b/brightchart-lib index c2f5cf9c..3092094b 160000 --- a/brightchart-lib +++ b/brightchart-lib @@ -1 +1 @@ -Subproject commit c2f5cf9ce8f849a12efb01c0534346524ca49345 +Subproject commit 3092094b450e14ffe4166785426a3277323bd5d4 diff --git a/brightchart-react-components b/brightchart-react-components index 6564539f..4d864a42 160000 --- a/brightchart-react-components +++ b/brightchart-react-components @@ -1 +1 @@ -Subproject commit 6564539fb99165d1d7be99cd129e412760d3d0ac +Subproject commit 4d864a42574f9c7b5c432e7c5d1d5fbae00fca5d diff --git a/brightchat-lib b/brightchat-lib index 6e54c95e..8c285272 160000 --- a/brightchat-lib +++ b/brightchat-lib @@ -1 +1 @@ -Subproject commit 6e54c95ee6e46c89e62a5eb0a16c92e80d0541b3 +Subproject commit 8c285272e62e916dd340f286376006377cea8c24 diff --git a/brightchat-react-components b/brightchat-react-components index 87959da7..f2d8b938 160000 --- a/brightchat-react-components +++ b/brightchat-react-components @@ -1 +1 @@ -Subproject commit 87959da75f6e6fb563fd93cdabb0089fde652d37 +Subproject commit f2d8b938e4e1d6b25cd096a2c00f84f8b228d2bd diff --git a/brightdate b/brightdate index b970bd9b..35821b78 160000 --- a/brightdate +++ b/brightdate @@ -1 +1 @@ -Subproject commit b970bd9ba667e7cdd8ba5fddcc83e7240c459035 +Subproject commit 35821b7854f6ce1d3dcbe5cf48c5b7f3d5464002 diff --git a/brighthub-lib b/brighthub-lib index 3fa18c04..5fffe374 160000 --- a/brighthub-lib +++ b/brighthub-lib @@ -1 +1 @@ -Subproject commit 3fa18c04900109b5f957fd1d168b544a412857a9 +Subproject commit 5fffe374c9c861ede2037e8b76e77522b5087c98 diff --git a/brighthub-react-components b/brighthub-react-components index 847a62c0..f038edf8 160000 --- a/brighthub-react-components +++ b/brighthub-react-components @@ -1 +1 @@ -Subproject commit 847a62c0a303fb2586a6dd5071a123b3b41d1d2a +Subproject commit f038edf8c5032e198f87767e8b74b7fc6092ab26 diff --git a/brightledger-assets-api-lib b/brightledger-assets-api-lib index 69ad117b..c23ea128 160000 --- a/brightledger-assets-api-lib +++ b/brightledger-assets-api-lib @@ -1 +1 @@ -Subproject commit 69ad117bcf9e61c0176424efd183658a146d551f +Subproject commit c23ea1289a5f50cf1d21691e6fe932a1e4506104 diff --git a/brightledger-assets-lib b/brightledger-assets-lib index 34456c27..21a9cd65 160000 --- a/brightledger-assets-lib +++ b/brightledger-assets-lib @@ -1 +1 @@ -Subproject commit 34456c278b5c4f515356398b9189d2d021a09a23 +Subproject commit 21a9cd65935b0232bea2b547027bf3514de42e61 diff --git a/brightledger-assets-react-components b/brightledger-assets-react-components index d3608cc6..26e02a3a 160000 --- a/brightledger-assets-react-components +++ b/brightledger-assets-react-components @@ -1 +1 @@ -Subproject commit d3608cc6ea1678fedb2168639437b7bca3b6ffb4 +Subproject commit 26e02a3aa6248529802d95b5d884b8e02091abc7 diff --git a/brightledger-metering-log-lib b/brightledger-metering-log-lib index 366857d1..80979b51 160000 --- a/brightledger-metering-log-lib +++ b/brightledger-metering-log-lib @@ -1 +1 @@ -Subproject commit 366857d1886cc6b240440fa8375ba04cca16be36 +Subproject commit 80979b512f78fa730817f0252ce39eddc0e52678 diff --git a/brightmail-lib b/brightmail-lib index 4fd41616..31c01ee3 160000 --- a/brightmail-lib +++ b/brightmail-lib @@ -1 +1 @@ -Subproject commit 4fd416162c6e4089f833f26a947bf98154da8c18 +Subproject commit 31c01ee3d1fef7d91cce55b8afe5115f0dcbf4c1 diff --git a/brightmail-react-components b/brightmail-react-components index cfcdbc77..23c694eb 160000 --- a/brightmail-react-components +++ b/brightmail-react-components @@ -1 +1 @@ -Subproject commit cfcdbc777ac07224e10f2ea1a5234e2ef1e0582c +Subproject commit 23c694ebb8b067bafe78fca1bbfb91094d88133f diff --git a/brightpass-lib b/brightpass-lib index 927ba3e1..ec926386 160000 --- a/brightpass-lib +++ b/brightpass-lib @@ -1 +1 @@ -Subproject commit 927ba3e14a6951c2c50a7af7fe483b41cd00ba39 +Subproject commit ec9263866d8bfebb19bde06e8966a9e9ffc2659a diff --git a/brightpass-react-components b/brightpass-react-components index 9e3a7471..bafbadad 160000 --- a/brightpass-react-components +++ b/brightpass-react-components @@ -1 +1 @@ -Subproject commit 9e3a7471110aafecab3c3959091427488a12acbe +Subproject commit bafbadad676f18e766fed554104ae0dad786ebde diff --git a/digitalburnbag-api-lib b/digitalburnbag-api-lib index cc048526..1d7612ee 160000 --- a/digitalburnbag-api-lib +++ b/digitalburnbag-api-lib @@ -1 +1 @@ -Subproject commit cc048526c4dcb52930a9cebe295af8fc45a052cd +Subproject commit 1d7612eea338595e1b8d2af68d7d44436bf9c69d diff --git a/digitalburnbag-desktop b/digitalburnbag-desktop index 69cbb9fd..4d1da95c 160000 --- a/digitalburnbag-desktop +++ b/digitalburnbag-desktop @@ -1 +1 @@ -Subproject commit 69cbb9fd680dd0ca2e8fa626f035370d468fc940 +Subproject commit 4d1da95c1db0ddc8f4fc643f0eabbe0cb3801237 diff --git a/digitalburnbag-lib b/digitalburnbag-lib index 63fd3432..fe819d15 160000 --- a/digitalburnbag-lib +++ b/digitalburnbag-lib @@ -1 +1 @@ -Subproject commit 63fd34328acfebcd67ab9badeaffdae386bc38e5 +Subproject commit fe819d15425364bb3adce55fa9a5013b44b6b417 diff --git a/digitalburnbag-react-components b/digitalburnbag-react-components index e968ee85..805a4ab1 160000 --- a/digitalburnbag-react-components +++ b/digitalburnbag-react-components @@ -1 +1 @@ -Subproject commit e968ee8581d9a37ee1cfd8f613839653e4bd8d7e +Subproject commit 805a4ab197b4c026055925124bee9a1645b12bd1 diff --git a/digitalburnbag-sync-client b/digitalburnbag-sync-client index 58cb67fb..eb7a6939 160000 --- a/digitalburnbag-sync-client +++ b/digitalburnbag-sync-client @@ -1 +1 @@ -Subproject commit 58cb67fb050c5f1586e5f9b0b6c51e7fb7f12e80 +Subproject commit eb7a6939b96dd9e475f569fbff0220af46e98814 diff --git a/docs/overview/brightchain-summary.md b/docs/overview/brightchain-summary.md index f9912470..0181067e 100644 --- a/docs/overview/brightchain-summary.md +++ b/docs/overview/brightchain-summary.md @@ -7,7 +7,7 @@ nav_order: 3 ## 1. Abstract -> *Where the state sees noise, the people find truth.* +> *BrightChain: Privacy you don't have to ask for.* BrightChain is a decentralized application platform and the foundation of BrightStack — a full-stack paradigm for building decentralized applications using BrightChain, Express, React, and Node.js. At its heart is a concept introduced by The Owner Free File System which breaks a file up into source blocks and merges them with blocks of random data using an "exclusive or" operation and discards the source blocks. Added on top of that, we introduce identity/anonymity, voting, reputation, block revocation and expiration, and a confidential BrightTrust. What the Owner Free Filesystem called "whitening", we call "Brightening" and where BrightChain gets its name. diff --git a/docs/papers/brightchain.md b/docs/papers/brightchain.md index 5ff25d5e..e676d43c 100644 --- a/docs/papers/brightchain.md +++ b/docs/papers/brightchain.md @@ -15,7 +15,7 @@ jessica@digitaldefiance.org --- -> *Building a world where ideas have no borders and truth has no owner.* +> *BrightChain: Privacy you don't have to ask for.* ## 1. Introduction diff --git a/jest.preset.js b/jest.preset.js index 52df2fbf..b04e445b 100644 --- a/jest.preset.js +++ b/jest.preset.js @@ -1,10 +1,26 @@ const nxPreset = require('@nx/jest/preset').default; +const path = require('path'); + +// Absolute paths work in both CJS and ESM jest.config.ts files, +// unlike require.resolve() which fails when the config is loaded as ESM. +const WORKSPACE_ROOT = __dirname; + +// When BRIGHTCHAIN_PROGRESS_FILE is set (i.e. launched via test-with-progress.mjs +// or e2e-with-progress.mjs), inject the custom reporter that emits per-test-case +// JSONL events for the live ETA progress bar and error log. +// This avoids the need to pass --jest-config which would replace per-project configs. +const progressReporters = process.env.BRIGHTCHAIN_PROGRESS_FILE + ? [ + 'default', + [path.join(WORKSPACE_ROOT, 'tools/custom-jest-reporter.js'), {}], + ] + : undefined; module.exports = { ...nxPreset, snapshotSerializers: [ ...(nxPreset.snapshotSerializers || []), - require.resolve('./brightchain-lib/bigIntSerializer.ts'), + path.join(WORKSPACE_ROOT, 'brightchain-lib/bigIntSerializer.ts'), ], testPathIgnorePatterns: [ ...(nxPreset.testPathIgnorePatterns || []), @@ -17,4 +33,6 @@ module.exports = { '/dist/', '/dist/', ], + // Only set reporters when the progress file env var is present + ...(progressReporters ? { reporters: progressReporters } : {}), }; diff --git a/jest.preset.monitored.js b/jest.preset.monitored.js new file mode 100644 index 00000000..d92e8535 --- /dev/null +++ b/jest.preset.monitored.js @@ -0,0 +1,30 @@ +const nxPreset = require('@nx/jest/preset').default; + +module.exports = { + ...nxPreset, + snapshotSerializers: [ + ...(nxPreset.snapshotSerializers || []), + require.resolve('./brightchain-lib/bigIntSerializer.ts'), + ], + testPathIgnorePatterns: [ + ...(nxPreset.testPathIgnorePatterns || []), + '/node_modules/', + '/dist/', + '/dist/', + ], + modulePathIgnorePatterns: [ + ...(nxPreset.modulePathIgnorePatterns || []), + '/dist/', + '/dist/', + ], + // Suppress duplicate manual mock warnings that arise when jest-haste-map + // scans the whole workspace and finds identically-named __mocks__ files + // in different projects (e.g. brightchain-lib.ts in multiple packages). + haste: { + throwOnModuleCollision: false, + }, + reporters: [ + 'default', + [require.resolve('./tools/custom-jest-reporter.js'), {}] + ] +}; diff --git a/package.json b/package.json index 9952dba1..7075591b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "brightchain", - "version": "0.32.0", + "version": "0.32.1", "license": "MIT", "workspaces": [ "brightdate", @@ -97,15 +97,21 @@ "aws:credentials": "aws sts get-session-token --duration-seconds 43200", "aws:install": "./install-aws.sh", "test:all:e2e:dev": "NODE_ENV=development NODE_OPTIONS='--max-old-space-size=16384' yarn nx run-many --target=e2e --configuration=development", + "test:all:e2e:dev:monitored": "NODE_ENV=development NODE_OPTIONS='--max-old-space-size=16384' yarn nx run-many --target=e2e --configuration=development --skip-nx-cache", + "test:all:e2e:dev:progress": "NODE_ENV=development NODE_OPTIONS='--max-old-space-size=16384' node tools/scripts/e2e-with-progress.mjs", "test:all:e2e:dev:nocache": "NODE_ENV=development NODE_OPTIONS='--max-old-space-size=16384' yarn nx run-many --target=e2e --configuration=development --skip-nx-cache", "test:all:e2e:dev:logged": "NODE_ENV=development NODE_OPTIONS='--max-old-space-size=16384' yarn nx run-many --target=e2e --configuration=development --outputStyle=stream 2>&1 | ansifilter -o e2e.log", - "test:all:e2e:dev:nocache:logged": "NODE_ENV=development NODE_OPTIONS='--max-old-space-size=16384' yarn nx run-many --target=e2e --configuration=development --outputStyle=stream --skip-nx-cache 2>&1 | ansifilter -o e2e.log", + "test:all:e2e:dev:nocache:logged": "NODE_ENV=development NODE_OPTIONS='--max-old-space-size=16384' yarn nx run-many --target=e2e --configuration=development --outputStyle=stream --skip-nx-cache --parallel=16 2>&1 | ansifilter -o e2e.log", "test:all:dev": "NODE_ENV=development yarn nx run-many --target=test --configuration=development", "test:all:dev:nocache": "NODE_ENV=development yarn nx run-many --target=test --configuration=development --skip-nx-cache", - "test:all:dev:nocache:logged": "NODE_ENV=development yarn nx run-many --target=test --configuration=development --skip-nx-cache 2>&1 | ansifilter -o test-dev.log", + "test:all:dev:nocache:logged": "NODE_ENV=development yarn nx run-many --target=test --configuration=development --skip-nx-cache --parallel=16 2>&1 | ansifilter -o test-dev.log", "test:all:dev:16": "NODE_ENV=development yarn nx run-many --target=test --configuration=development --parallel=16", + "test:all:dev:16:monitored": "NODE_ENV=development node tools/scripts/test-with-progress.mjs", + "test:all:dev:16:progress": "NODE_ENV=development node tools/scripts/test-with-progress.mjs", + "test:count": "node tools/scripts/count-tests.mjs", + "test:count:refresh": "node tools/scripts/count-tests.mjs --no-cache", "test:all:dev:16:logged": "NODE_ENV=development yarn nx run-many --skip-nx-cache --target=test --configuration=development --parallel=16 --outputStyle=stream 2>&1 | ansifilter -o test-dev.log", - "test:all:dev:logged": "NODE_ENV=development yarn nx run-many --skip-nx-cache --target=test --configuration=development --outputStyle=stream 2>&1 | ansifilter -o test-dev.log", + "test:all:dev:logged": "NODE_ENV=development yarn nx run-many --skip-nx-cache --target=test --configuration=development --parallel=16 --outputStyle=stream 2>&1 | ansifilter -o test-dev.log", "test:enclave:required": "REQUIRE_SECURE_ENCLAVE=true NODE_ENV=development npx nx test brightchain-api-lib --configuration=development", "test:ci": "NODE_ENV=test npx nx test brightchain-lib --parallel=16 --skip-nx-cache --configuration=ci", "test:azure-store": "yarn nx test brightchain-azure-store", @@ -148,24 +154,24 @@ "@aws-sdk/client-ses": "^3.1024.0", "@azure/identity": "^4.6.0", "@azure/storage-blob": "^12.26.0", - "@brightchain/brightcal-api-lib": "^0.32.0", - "@brightchain/brightcal-lib": "^0.32.0", - "@brightchain/brightcal-react-components": "^0.32.0", - "@brightchain/brightchain-api-lib": "^0.32.0", - "@brightchain/brightchain-lib": "^0.32.0", - "@brightchain/brightchain-react-components": "^0.32.0", - "@brightchain/brightchat-lib": "^0.32.0", - "@brightchain/brightdate": "^0.32.0", - "@brightchain/brighthub-lib": "^0.32.0", - "@brightchain/brighthub-react-components": "^0.32.0", - "@brightchain/brightmail-lib": "^0.32.0", - "@brightchain/brightmail-react-components": "^0.32.0", - "@brightchain/brightpass-lib": "^0.32.0", - "@brightchain/brightpass-react-components": "^0.32.0", - "@brightchain/db": "^0.32.0", - "@brightchain/digitalburnbag-api-lib": "^0.32.0", - "@brightchain/digitalburnbag-lib": "^0.32.0", - "@brightchain/digitalburnbag-react-components": "^0.32.0", + "@brightchain/brightcal-api-lib": "^0.32.1", + "@brightchain/brightcal-lib": "^0.32.1", + "@brightchain/brightcal-react-components": "^0.32.1", + "@brightchain/brightchain-api-lib": "^0.32.1", + "@brightchain/brightchain-lib": "^0.32.1", + "@brightchain/brightchain-react-components": "^0.32.1", + "@brightchain/brightchat-lib": "^0.32.1", + "@brightchain/brightdate": "^0.32.2", + "@brightchain/brighthub-lib": "^0.32.1", + "@brightchain/brighthub-react-components": "^0.32.1", + "@brightchain/brightmail-lib": "^0.32.1", + "@brightchain/brightmail-react-components": "^0.32.1", + "@brightchain/brightpass-lib": "^0.32.1", + "@brightchain/brightpass-react-components": "^0.32.1", + "@brightchain/db": "^0.32.1", + "@brightchain/digitalburnbag-api-lib": "^0.32.1", + "@brightchain/digitalburnbag-lib": "^0.32.1", + "@brightchain/digitalburnbag-react-components": "^0.32.1", "@digitaldefiance/branded-enum": "0.0.7", "@digitaldefiance/branded-interface": "0.0.5", "@digitaldefiance/bzip2-wasm": "^1.1.1", diff --git a/showcase/package.json b/showcase/package.json index 4f0cfa0a..c7f43525 100644 --- a/showcase/package.json +++ b/showcase/package.json @@ -14,8 +14,8 @@ }, "dependencies": { "@awesome.me/kit-a20d532681": "^1.0.23", - "@brightchain/brightchain-lib": "^0.32.0", - "@brightchain/digitalburnbag-react-components": "^0.32.0", + "@brightchain/brightchain-lib": "^0.32.1", + "@brightchain/digitalburnbag-react-components": "^0.32.1", "@digitaldefiance/ecies-lib": "5.2.2", "@digitaldefiance/i18n-lib": "4.7.5", "@ethereumjs/wallet": "^10.0.0", diff --git a/showcase/src/App.tsx b/showcase/src/App.tsx index b6f84210..f73981b9 100644 --- a/showcase/src/App.tsx +++ b/showcase/src/App.tsx @@ -10,6 +10,7 @@ import BlogPost from './components/BlogPost'; import { BrightChainSoupDemo } from './components/BrightChainSoupDemo'; import { CompatibilityWarning } from './components/CompatibilityWarning'; import Components from './components/Components'; +import DatePage from './components/DatePage'; import Demo from './components/Demo'; import { DemoNavigation } from './components/DemoNavigation'; import { EducationalModeProvider } from './components/EducationalModeProvider'; @@ -52,6 +53,9 @@ function App() { } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/showcase/src/components/BrightChainLogoI18N.tsx b/showcase/src/components/BrightChainLogoI18N.tsx new file mode 100644 index 00000000..ae83baaa --- /dev/null +++ b/showcase/src/components/BrightChainLogoI18N.tsx @@ -0,0 +1,161 @@ +import { CONSTANTS } from '@brightchain/brightchain-lib'; +import { BrightChainLogo } from '@brightchain/brightchain-react-components'; +import React from 'react'; +import { useShowcaseI18n } from '../i18n/ShowcaseI18nContext'; +import { ShowcaseStrings } from '../i18n/showcaseStrings'; + +export interface BrightChainLogoI18NProps { + primaryColor?: string; + primarySecondaryBreak?: boolean; + secondaryColor?: string; + taglineColor?: string; + taglineText?: string; + className?: string; + style?: React.CSSProperties; + width?: number | string; + height?: number | string; +} + +export const BrightChainLogoI18N: React.FC = ({ + primaryColor: brightColor = CONSTANTS.THEME_COLORS.CHAIN_BLUE_DARK, + primarySecondaryBreak = false, + secondaryColor: chainColor = CONSTANTS.THEME_COLORS.CHAIN_BLUE_LIGHT, + taglineColor = '#ffffff', + taglineText, + className, + style, + width, + height = 600, +}) => { + const { t, language } = useShowcaseI18n(); + + // Use the reactive currentLanguage from the i18n engine + // so the logo updates when the language dropdown changes. + const languageCode = language ?? 'en-US'; + + type LogoBrandConfig = { + first: { text: string; color: 'primary' | 'secondary' }; + second: { text: string; color: 'primary' | 'secondary' }; + break: boolean; + }; + + const logoBrandConfig: Record = { + 'en-US': { + first: { + text: t(ShowcaseStrings.Common_Bright, undefined, 'en-US'), + color: 'primary', + }, + second: { + text: t(ShowcaseStrings.Common_Chain, undefined, 'en-US'), + color: 'secondary', + }, + break: false, + }, + 'en-GB': { + first: { + text: t(ShowcaseStrings.Common_Bright, undefined, 'en-GB'), + color: 'primary', + }, + second: { + text: t(ShowcaseStrings.Common_Chain, undefined, 'en-GB'), + color: 'secondary', + }, + break: false, + }, + de: { + first: { + text: t(ShowcaseStrings.Common_Bright, undefined, 'de'), + color: 'primary', + }, + second: { + text: t(ShowcaseStrings.Common_Chain, undefined, 'de'), + color: 'secondary', + }, + break: false, + }, + ja: { + first: { + text: t(ShowcaseStrings.Common_Bright, undefined, 'ja'), + color: 'primary', + }, + second: { + text: t(ShowcaseStrings.Common_Chain, undefined, 'ja'), + color: 'secondary', + }, + break: false, + }, + 'zh-CN': { + first: { + text: t(ShowcaseStrings.Common_Bright, undefined, 'zh-CN'), + color: 'primary', + }, + second: { + text: t(ShowcaseStrings.Common_Chain, undefined, 'zh-CN'), + color: 'secondary', + }, + break: false, + }, + // Adjective-after-noun languages: "Chain" first, "Bright" second + fr: { + first: { + text: t(ShowcaseStrings.Common_Chain, undefined, 'fr'), + color: 'secondary', + }, + second: { + text: t(ShowcaseStrings.Common_Bright, undefined, 'fr'), + color: 'primary', + }, + break: false, + }, + es: { + first: { + text: t(ShowcaseStrings.Common_Chain, undefined, 'es'), + color: 'secondary', + }, + second: { + text: t(ShowcaseStrings.Common_Bright, undefined, 'es'), + color: 'primary', + }, + break: false, + }, + // Same order as English, but needs space (not a compound word) + uk: { + first: { + text: t(ShowcaseStrings.Common_Bright, undefined, 'uk'), + color: 'primary', + }, + second: { + text: t(ShowcaseStrings.Common_Chain, undefined, 'uk'), + color: 'secondary', + }, + break: true, + }, + }; + + const primaryText = + logoBrandConfig[languageCode]?.first.text ?? + logoBrandConfig['en-US']?.first.text; + const secondaryText = + logoBrandConfig[languageCode]?.second.text ?? + logoBrandConfig['en-US']?.second.text; + + return BrightChainLogo({ + primaryColor: brightColor, + primaryText: primaryText, + primarySecondaryBreak, + secondaryColor: chainColor, + secondaryText: secondaryText, + taglineColor, + taglineText: + taglineText ?? + t( + ShowcaseStrings.Common_PrivacyParticipationPower, + undefined, + languageCode, + ), + className, + style, + width, + height, + }); +}; diff --git a/showcase/src/components/BrightChainLogoI18NRotation.tsx b/showcase/src/components/BrightChainLogoI18NRotation.tsx new file mode 100644 index 00000000..ea53f181 --- /dev/null +++ b/showcase/src/components/BrightChainLogoI18NRotation.tsx @@ -0,0 +1,67 @@ +import { CONSTANTS } from '@brightchain/brightchain-lib'; +import { FC, useEffect, useMemo, useState } from 'react'; +import { useShowcaseI18n } from '../i18n/ShowcaseI18nContext'; +import { ShowcaseStrings } from '../i18n/showcaseStrings'; +import { BrightChainLogoI18N } from './BrightChainLogoI18N'; + +export interface BrightChainLogoI18NRotationProps { + primaryColor?: string; + primarySecondaryBreak?: boolean; + secondaryColor?: string; + taglineColor?: string; + className?: string; + style?: React.CSSProperties; + width?: number | string; + height?: number | string; +} + +export const BrightChainLogoI18NRotation: FC< + BrightChainLogoI18NRotationProps +> = ({ + primaryColor: brightColor = CONSTANTS.THEME_COLORS.CHAIN_BLUE_DARK, + primarySecondaryBreak = false, + secondaryColor: chainColor = CONSTANTS.THEME_COLORS.CHAIN_BLUE_LIGHT, + taglineColor = '#ffffff', + className, + style, + width = 3151, + height = 649, +}) => { + const slogans = useMemo( + () => [ + ShowcaseStrings.Slogan_Math_Search_Warrant, + ShowcaseStrings.Slogan_Signal_Belongs_To_You, + ShowcaseStrings.Slogan_Defiance_By_Design, + ShowcaseStrings.Slogan_BrightChain_Privacy, + ShowcaseStrings.Slogan_Speak_Freely, + ShowcaseStrings.Slogan_Distributed_By_Many, + ShowcaseStrings.Slogan_Truth_In_The_Signal, + ShowcaseStrings.Slogan_Ideas_Paper_Trail, + ], + [], + ); + const { t } = useShowcaseI18n(); + const [currentIndex, setCurrentIndex] = useState(0); + + useEffect(() => { + const intervalId = setInterval(() => { + setCurrentIndex((prev) => (prev + 1) % slogans.length); + }, 5000); + + return () => clearInterval(intervalId); + }, [slogans.length]); + + return ( + + ); +}; diff --git a/showcase/src/components/BrightDate.tsx b/showcase/src/components/BrightDate.tsx new file mode 100644 index 00000000..b779f7fd --- /dev/null +++ b/showcase/src/components/BrightDate.tsx @@ -0,0 +1,65 @@ +import { toBrightDateString } from '@brightchain/brightchain-lib'; +import { toDate } from '@brightchain/brightdate'; +import { FC, useEffect, useMemo, useState } from 'react'; +import { useShowcaseI18n } from '../i18n/ShowcaseI18nContext'; +import { ShowcaseStrings } from '../i18n/showcaseStrings'; + +export interface BrightDateProps { + /** + * The refresh interval in ms. 0 or negative to disable auto-refresh. + */ + interval?: number; + format?: 'full' | 'standard' | 'compact'; + /** + * A JavaScript Date to display. When provided, auto-refresh is disabled. + */ + date?: Date; + /** + * A BrightDate numeric value (decimal days since J2000.0) to display. + * When provided, auto-refresh is disabled. Takes precedence over `date`. + */ + value?: number; +} + +const PRECISION: Record, number> = { + full: 8, // sub-millisecond + standard: 5, // ~0.86s + compact: 3, // ~86s +}; + +export const BrightDate: FC = ({ + interval = 1000, + format = 'full', + date, + value, +}) => { + const { t } = useShowcaseI18n(); + const [now, setNow] = useState(new Date()); + + // Convert a BrightDate numeric value to a JS Date + const resolvedDate = useMemo(() => { + if (value !== undefined) return toDate(value); + return date; + }, [value, date]); + + useEffect(() => { + if (resolvedDate !== undefined) { + setNow(resolvedDate); + return; + } + if (interval <= 0) { + setNow(new Date()); + return; + } + const timer = setInterval(() => setNow(new Date()), interval); + return () => clearInterval(timer); + }, [interval, resolvedDate]); + + const brightDate = toBrightDateString(now, PRECISION[format]); + + return ( + + ); +}; diff --git a/showcase/src/components/DatePage.tsx b/showcase/src/components/DatePage.tsx new file mode 100644 index 00000000..50937bbf --- /dev/null +++ b/showcase/src/components/DatePage.tsx @@ -0,0 +1,523 @@ +/** + * DatePage — Displays the current date and time in multiple formats, + * including BrightDate, and shows known holidays for today using date-holidays. + */ + +import { faCalendarClock } from '@awesome.me/kit-a20d532681/icons/classic/regular'; +import { + nowAsBrightDate, + toBrightDateString, +} from '@brightchain/brightchain-lib'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + Box, + Card, + CardContent, + Chip, + Container, + Divider, + Link, + Tooltip, + Typography, +} from '@mui/material'; +import Holidays from 'date-holidays'; +import { FC, useEffect, useMemo, useState } from 'react'; +import { useShowcaseI18n } from '../i18n/ShowcaseI18nContext'; +import { ShowcaseStrings } from '../i18n/showcaseStrings'; +import { BrightDate } from './BrightDate'; + +// ─── Date Format Helpers ──────────────────────────────────────────────────── + +function getJulianDate(date: Date): number { + return date.getTime() / 86400000 + 2440587.5; +} + +function getModifiedJulianDate(date: Date): number { + return getJulianDate(date) - 2400000.5; +} + +function getUnixTimestamp(date: Date): number { + return Math.floor(date.getTime() / 1000); +} + +function getDayOfYear(date: Date): number { + const start = new Date(date.getFullYear(), 0, 0); + const diff = date.getTime() - start.getTime(); + return Math.floor(diff / (1000 * 60 * 60 * 24)); +} + +function getISOWeek(date: Date): number { + const d = new Date( + Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()), + ); + const dayNum = d.getUTCDay() || 7; + d.setUTCDate(d.getUTCDate() + 4 - dayNum); + const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7); +} + +function isLeapYear(year: number): boolean { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; +} + +// ─── Interplanetary Telemetry Helpers ─────────────────────────────────────── + +/** Speed of light in km/s */ +const SPEED_OF_LIGHT_KM_S = 299_792.458; + +/** Seconds per day */ +const SECONDS_PER_DAY = 86_400; + +/** Average Earth–Moon distance in km */ +const MOON_DISTANCE_KM = 384_400; + +/** Minimum Earth–Mars distance in km (opposition) */ +const MARS_MIN_DISTANCE_KM = 54_600_000; + +/** Maximum Earth–Mars distance in km (conjunction) */ +const MARS_MAX_DISTANCE_KM = 401_000_000; + +/** + * Convert seconds to millidays. + * 1 milliday = 86.4 seconds. + */ +function secondsToMillidays(seconds: number): number { + return (seconds / SECONDS_PER_DAY) * 1000; +} + +/** + * Estimate current Earth–Mars distance using a simplified sinusoidal model. + * Mars synodic period ≈ 779.94 days. We approximate the distance as oscillating + * between min and max over this period, with a reference opposition date. + */ +function estimateMarsDistanceKm(date: Date): number { + const SYNODIC_PERIOD_DAYS = 779.94; + // Reference opposition: December 8, 2022 + const referenceOpposition = new Date('2022-12-08T00:00:00Z'); + const daysSinceOpposition = + (date.getTime() - referenceOpposition.getTime()) / (1000 * 60 * 60 * 24); + // Phase: 0 at opposition (min distance), π at conjunction (max distance) + const phase = (2 * Math.PI * daysSinceOpposition) / SYNODIC_PERIOD_DAYS; + const midpoint = (MARS_MIN_DISTANCE_KM + MARS_MAX_DISTANCE_KM) / 2; + const amplitude = (MARS_MAX_DISTANCE_KM - MARS_MIN_DISTANCE_KM) / 2; + // cos(phase) = 1 at opposition → min distance; cos(phase) = -1 at conjunction → max + return midpoint - amplitude * Math.cos(phase); +} + +// ─── Holiday Type → Chip Color ────────────────────────────────────────────── + +const HOLIDAY_CHIP_COLORS: Record< + string, + 'primary' | 'secondary' | 'success' | 'info' | 'warning' +> = { + public: 'primary', + bank: 'info', + observance: 'success', + school: 'warning', + optional: 'secondary', +}; + +/** + * Map date-holidays type to our i18n string key. + */ +function holidayTypeLabel(type: string, t: (key: string) => string): string { + switch (type) { + case 'public': + return t(ShowcaseStrings.DatePage_HolidayType_Public); + case 'bank': + return t(ShowcaseStrings.DatePage_HolidayType_Bank); + case 'observance': + return t(ShowcaseStrings.DatePage_HolidayType_Observance); + default: + return t(ShowcaseStrings.DatePage_HolidayType_Observance); + } +} + +// ─── Component ────────────────────────────────────────────────────────────── + +export const DatePage: FC = () => { + const { t, language } = useShowcaseI18n(); + const [now, setNow] = useState(new Date()); + + // Update every second + useEffect(() => { + const interval = setInterval(() => setNow(new Date()), 1000); + return () => clearInterval(interval); + }, []); + + // Initialize date-holidays for the user's locale + const hd = useMemo(() => { + // Map language codes to country codes for date-holidays + // Default to US; extend as needed + const langToCountry: Record = { + en: 'US', + 'en-US': 'US', + 'en-GB': 'GB', + de: 'DE', + 'de-DE': 'DE', + fr: 'FR', + 'fr-FR': 'FR', + es: 'ES', + 'es-ES': 'ES', + ja: 'JP', + 'ja-JP': 'JP', + zh: 'CN', + 'zh-CN': 'CN', + uk: 'UA', + 'uk-UA': 'UA', + }; + const country = langToCountry[language] || 'US'; + const langCode = language?.split('-')[0] || 'en'; + return new Holidays(country, { languages: [langCode, 'en'] }); + }, [language]); + + // Get holidays for today + const holidays = useMemo(() => { + const result = hd.isHoliday(now); + if (!result) return []; + return result; + }, [hd, now]); + + // BrightDate values at different precisions + const brightDateFull = nowAsBrightDate(8); // sub-millisecond + const brightDateStandard = toBrightDateString(now, 5); // ~0.86s + const brightDateCompact = toBrightDateString(now, 3); // ~86s + + const julianDate = getJulianDate(now); + const mjd = getModifiedJulianDate(now); + const unixTs = getUnixTimestamp(now); + const dayOfYear = getDayOfYear(now); + const isoWeek = getISOWeek(now); + const daysInYear = isLeapYear(now.getFullYear()) ? 366 : 365; + + // ─── Interplanetary Telemetry (Cochrane Easter Egg) ───────────────────── + const moonLightDelaySec = MOON_DISTANCE_KM / SPEED_OF_LIGHT_KM_S; + const moonDelayMillidays = secondsToMillidays(moonLightDelaySec); + + const marsMinDelaySec = MARS_MIN_DISTANCE_KM / SPEED_OF_LIGHT_KM_S; + const marsMinDelayMillidays = secondsToMillidays(marsMinDelaySec); + + const marsMaxDelaySec = MARS_MAX_DISTANCE_KM / SPEED_OF_LIGHT_KM_S; + const marsMaxDelayMillidays = secondsToMillidays(marsMaxDelaySec); + + const marsCurrentDistanceKm = estimateMarsDistanceKm(now); + const marsCurrentDelaySec = marsCurrentDistanceKm / SPEED_OF_LIGHT_KM_S; + const marsCurrentDelayMillidays = secondsToMillidays(marsCurrentDelaySec); + + const formats: Array<{ label: string; value: string }> = [ + { + label: t(ShowcaseStrings.DatePage_Format_BrightDateFull), + value: t(ShowcaseStrings.Date_BrightDateTemplate, { + BD: brightDateFull, + }), + }, + { + label: t(ShowcaseStrings.DatePage_Format_BrightDateStandard), + value: t(ShowcaseStrings.Date_BrightDateTemplate, { + BD: brightDateStandard, + }), + }, + { + label: t(ShowcaseStrings.DatePage_Format_BrightDateCompact), + value: t(ShowcaseStrings.Date_BrightDateTemplate, { + BD: brightDateCompact, + }), + }, + { + label: t(ShowcaseStrings.DatePage_Format_ISO8601), + value: now.toISOString(), + }, + { + label: t(ShowcaseStrings.DatePage_Format_UTC), + value: now.toUTCString(), + }, + { + label: t(ShowcaseStrings.DatePage_Format_LocalDateTime), + value: now.toLocaleString(), + }, + { + label: t(ShowcaseStrings.DatePage_Format_LocalDate), + value: now.toLocaleDateString(undefined, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }), + }, + { + label: t(ShowcaseStrings.DatePage_Format_LocalTime), + value: now.toLocaleTimeString(), + }, + { + label: t(ShowcaseStrings.DatePage_Format_UnixTimestamp), + value: String(unixTs), + }, + { + label: t(ShowcaseStrings.DatePage_Format_UnixMs), + value: String(now.getTime()), + }, + { + label: t(ShowcaseStrings.DatePage_Format_JulianDate), + value: julianDate.toFixed(5), + }, + { + label: t(ShowcaseStrings.DatePage_Format_ModifiedJulianDate), + value: mjd.toFixed(5), + }, + { + label: t(ShowcaseStrings.DatePage_Format_DayOfYear), + value: `${dayOfYear} / ${daysInYear}`, + }, + { + label: t(ShowcaseStrings.DatePage_Format_ISOWeek), + value: `W${String(isoWeek).padStart(2, '0')}`, + }, + { + label: t(ShowcaseStrings.DatePage_Format_RFC2822), + value: now.toString(), + }, + ]; + + return ( +
+ + + + + {t(ShowcaseStrings.DatePage_Title)} + + + + {/* BrightDate Hero */} + + + + {t(ShowcaseStrings.DatePage_BrightDateEpochLabel)} + + + + + + {now.toLocaleDateString(undefined, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + })} + {' · '} + {now.toLocaleTimeString()} + + + + + {/* Holidays */} + {holidays.length > 0 && ( + + + + {t(ShowcaseStrings.DatePage_HolidaysTitle)} + + + {holidays.map((h) => ( + + ))} + + + + )} + + {/* All Formats */} + + + + {t(ShowcaseStrings.DatePage_AllFormatsTitle)} + + + + {formats.map(({ label, value }) => ( + + + {label} + + + {value} + + + ))} + + + + + {/* Epoch Reference */} + + + + + {t(ShowcaseStrings.DatePage_AboutBrightDateTitle)} + + + + {t(ShowcaseStrings.DatePage_AboutBrightDate_Epoch)} + + + {t(ShowcaseStrings.DatePage_AboutBrightDate_Fraction)} + + + {t(ShowcaseStrings.DatePage_AboutBrightDate_NoTimezones)} + + + + + {/* Cochrane Easter Egg: Interplanetary Telemetry */} + + + + 🛰️ {t(ShowcaseStrings.DatePage_Telemetry_Title)} + + + {t(ShowcaseStrings.DatePage_Telemetry_Subtitle)} + + + + {/* Moon */} + + {t(ShowcaseStrings.DatePage_Telemetry_MoonDelay)} + + + + {moonDelayMillidays.toFixed(4)}{' '} + {t(ShowcaseStrings.DatePage_Telemetry_Unit)} + + + + {/* Mars min */} + + {t(ShowcaseStrings.DatePage_Telemetry_MarsDelayMin)} + + + + {marsMinDelayMillidays.toFixed(4)}{' '} + {t(ShowcaseStrings.DatePage_Telemetry_Unit)} + + + + {/* Mars max */} + + {t(ShowcaseStrings.DatePage_Telemetry_MarsDelayMax)} + + + + {marsMaxDelayMillidays.toFixed(4)}{' '} + {t(ShowcaseStrings.DatePage_Telemetry_Unit)} + + + + {/* Mars current estimate */} + + {t(ShowcaseStrings.DatePage_Telemetry_MarsDelayCurrent)} + + + + {marsCurrentDelayMillidays.toFixed(4)}{' '} + {t(ShowcaseStrings.DatePage_Telemetry_Unit)} + + + + + {t(ShowcaseStrings.DatePage_Telemetry_Footer)} + + + + +
+ ); +}; + +export default DatePage; diff --git a/showcase/src/components/DemoNavigation.tsx b/showcase/src/components/DemoNavigation.tsx index b9815316..2a4d3a34 100644 --- a/showcase/src/components/DemoNavigation.tsx +++ b/showcase/src/components/DemoNavigation.tsx @@ -6,7 +6,7 @@ import { ShowcaseStringKey, ShowcaseStrings } from '../i18n/showcaseStrings'; import './DemoNavigation.css'; import { LanguageSelector } from './LanguageSelector'; -import brightchainTxImg from '../assets/images/brightchain-tx-white.png'; +import { BrightChainLogoI18N } from './BrightChainLogoI18N'; export type DemoMode = | 'home' @@ -15,7 +15,8 @@ export type DemoMode = | 'minimal' | 'blog' | 'ledger' - | 'faq'; + | 'faq' + | 'date'; interface NavigationItem { id: DemoMode; @@ -61,6 +62,13 @@ const navigationItems: NavigationItem[] = [ icon: '❓', path: '/faq', }, + { + id: 'date', + label: ShowcaseStrings.Nav_BrightDate, + description: ShowcaseStrings.Nav_BrightDate_Description, + icon: '📅', + path: '/date', + }, ]; interface ExternalLink { @@ -122,11 +130,7 @@ export const DemoNavigation: React.FC = ({ >
- BrightChain + {/* Desktop Navigation */} diff --git a/showcase/src/components/ErrorVisualization.property.spec.tsx b/showcase/src/components/ErrorVisualization.property.spec.tsx index 53fbd97b..cf07a72f 100644 --- a/showcase/src/components/ErrorVisualization.property.spec.tsx +++ b/showcase/src/components/ErrorVisualization.property.spec.tsx @@ -8,6 +8,12 @@ * error messages and visual indicators for all error conditions. */ +// Async React property tests can exceed 5 s under parallel load — give them room. +// Vitest equivalent: set timeout via vi.setConfig (no retryTimes in Vitest core; +// use vitest.config retry option for suite-level retries). +import { vi } from 'vitest'; +vi.setConfig({ testTimeout: 30000 }); + import { act, render, renderHook, screen } from '@testing-library/react'; import { describe, expect, it } from 'vitest'; import { diff --git a/showcase/src/components/Hero.tsx b/showcase/src/components/Hero.tsx index ffe433db..31d07f63 100644 --- a/showcase/src/components/Hero.tsx +++ b/showcase/src/components/Hero.tsx @@ -1,11 +1,12 @@ import { motion } from 'framer-motion'; -import { FC, useEffect, useMemo } from 'react'; +import { FC } from 'react'; import { FaGithub, FaLock, FaNewspaper } from 'react-icons/fa'; import { GiCookingPot } from 'react-icons/gi'; import { Link } from 'react-router-dom'; import { useShowcaseI18n } from '../i18n/ShowcaseI18nContext'; import { ShowcaseStrings } from '../i18n/showcaseStrings'; import { AnimatedParticles } from './AnimatedParticles'; +import { BrightChainLogoI18N } from './BrightChainLogoI18N'; import './Hero.css'; import { ScrollIndicator } from './ScrollIndicator'; import { SloganRotation } from './SloganRotation'; @@ -14,7 +15,7 @@ interface HeroProps { scrollY: number; } -const Hero = ({ scrollY }: HeroProps) => { +const Hero: FC = ({ scrollY }: HeroProps) => { const { t } = useShowcaseI18n(); const parallaxOffset = scrollY * 0.5; @@ -48,10 +49,7 @@ const Hero = ({ scrollY }: HeroProps) => { animate={{ opacity: 1 }} transition={{ delay: 0.2, duration: 0.8 }} > - + diff --git a/showcase/src/components/SloganRotation.tsx b/showcase/src/components/SloganRotation.tsx index c3cfbb9f..fec6079f 100644 --- a/showcase/src/components/SloganRotation.tsx +++ b/showcase/src/components/SloganRotation.tsx @@ -1,5 +1,5 @@ import { motion } from 'framer-motion'; -import { FC, useEffect, useMemo } from 'react'; +import { FC, useEffect, useMemo, useState } from 'react'; import { useShowcaseI18n } from '../i18n/ShowcaseI18nContext'; import { ShowcaseStrings } from '../i18n/showcaseStrings'; import './SloganRotation.css'; @@ -8,32 +8,26 @@ export const SloganRotation: FC = () => { const slogans = useMemo( () => [ ShowcaseStrings.Slogan_Math_Search_Warrant, - ShowcaseStrings.Slogan_Where_State_Sees_Noise, - ShowcaseStrings.Slogan_Building_World_Where_Ideas_Have_No_Borders, + ShowcaseStrings.Slogan_Signal_Belongs_To_You, ShowcaseStrings.Slogan_Defiance_By_Design, + ShowcaseStrings.Slogan_BrightChain_Privacy, + ShowcaseStrings.Slogan_Speak_Freely, + ShowcaseStrings.Slogan_Distributed_By_Many, + ShowcaseStrings.Slogan_Truth_In_The_Signal, + ShowcaseStrings.Slogan_Ideas_Paper_Trail, ], [], ); const { t } = useShowcaseI18n(); + const [currentIndex, setCurrentIndex] = useState(0); useEffect(() => { - let currentIndex = 0; - const sloganElement = document.querySelector( - '.badge-slogan', - ) as HTMLElement; - - if (!sloganElement) return; - - const rotateSlogan = () => { - sloganElement.textContent = t(slogans[currentIndex]); - currentIndex = (currentIndex + 1) % slogans.length; - }; - - rotateSlogan(); // Show the first slogan immediately - const intervalId = setInterval(rotateSlogan, 5000); // Change every 5 seconds + const intervalId = setInterval(() => { + setCurrentIndex((prev) => (prev + 1) % slogans.length); + }, 5000); return () => clearInterval(intervalId); - }, [slogans, t]); + }, [slogans.length]); return ( { animate={{ opacity: 1, scale: 1 }} transition={{ delay: 0.1, duration: 0.6 }} > - {t(slogans[0])} + {t(slogans[currentIndex])} ); }; \ No newline at end of file diff --git a/showcase/src/i18n/ShowcaseI18nContext.tsx b/showcase/src/i18n/ShowcaseI18nContext.tsx index 2357e036..193874c5 100644 --- a/showcase/src/i18n/ShowcaseI18nContext.tsx +++ b/showcase/src/i18n/ShowcaseI18nContext.tsx @@ -39,7 +39,11 @@ const languageLoaders: Record< export interface ShowcaseI18nContextValue { language: CoreLanguageCode; setLanguage: (lang: CoreLanguageCode) => void; - t: (key: ShowcaseStringKey, params?: Record) => string; + t: ( + key: ShowcaseStringKey, + params?: Record, + lang?: string, + ) => string; isLoading: boolean; } @@ -171,10 +175,19 @@ export const ShowcaseI18nProvider: React.FC<{ children: React.ReactNode }> = ({ }, []); const t = useCallback( - (key: ShowcaseStringKey, params?: Record): string => { + ( + key: ShowcaseStringKey, + params?: Record, + lang?: string, + ): string => { // stringsVersion is captured to ensure React re-renders after language loads void stringsVersion; - return engine.safeTranslate(ShowcaseComponentId, key, params, language); + return engine.safeTranslate( + ShowcaseComponentId, + key, + params, + lang ?? language, + ); }, [engine, language, stringsVersion], ); diff --git a/showcase/src/i18n/showcaseStrings.ts b/showcase/src/i18n/showcaseStrings.ts index 253ed42f..1b3152b4 100644 --- a/showcase/src/i18n/showcaseStrings.ts +++ b/showcase/src/i18n/showcaseStrings.ts @@ -6,6 +6,55 @@ import { export const ShowcaseComponentId = 'Showcase'; export const ShowcaseStrings = createI18nStringKeys(ShowcaseComponentId, { + Common_Bright: 'Common_Bright', + Common_Chain: 'Common_Chain', + + Common_Privacy: 'Common_Privacy', + Common_Participation: 'Common_Participation', + Common_Power: 'Common_Power', + Common_PrivacyParticipationPower: 'Common_PrivacyParticipationPower', + + Date_BrightDateTemplate: 'Date_BrightDateTemplate', + + // ── DatePage ── + DatePage_Title: 'DatePage_Title', + DatePage_BrightDateEpochLabel: 'DatePage_BrightDateEpochLabel', + DatePage_HolidaysTitle: 'DatePage_HolidaysTitle', + DatePage_AllFormatsTitle: 'DatePage_AllFormatsTitle', + DatePage_AboutBrightDateTitle: 'DatePage_AboutBrightDateTitle', + DatePage_AboutBrightDate_Epoch: 'DatePage_AboutBrightDate_Epoch', + DatePage_AboutBrightDate_Fraction: 'DatePage_AboutBrightDate_Fraction', + DatePage_AboutBrightDate_NoTimezones: 'DatePage_AboutBrightDate_NoTimezones', + DatePage_Format_BrightDateFull: 'DatePage_Format_BrightDateFull', + DatePage_Format_BrightDateCompact: 'DatePage_Format_BrightDateCompact', + DatePage_Format_BrightDateStandard: 'DatePage_Format_BrightDateStandard', + DatePage_Format_ISO8601: 'DatePage_Format_ISO8601', + DatePage_Format_UTC: 'DatePage_Format_UTC', + DatePage_Format_LocalDateTime: 'DatePage_Format_LocalDateTime', + DatePage_Format_LocalDate: 'DatePage_Format_LocalDate', + DatePage_Format_LocalTime: 'DatePage_Format_LocalTime', + DatePage_Format_UnixTimestamp: 'DatePage_Format_UnixTimestamp', + DatePage_Format_UnixMs: 'DatePage_Format_UnixMs', + DatePage_Format_JulianDate: 'DatePage_Format_JulianDate', + DatePage_Format_ModifiedJulianDate: 'DatePage_Format_ModifiedJulianDate', + DatePage_Format_DayOfYear: 'DatePage_Format_DayOfYear', + DatePage_Format_ISOWeek: 'DatePage_Format_ISOWeek', + DatePage_Format_RFC2822: 'DatePage_Format_RFC2822', + DatePage_HolidayType_Public: 'DatePage_HolidayType_Public', + DatePage_HolidayType_Bank: 'DatePage_HolidayType_Bank', + DatePage_HolidayType_Observance: 'DatePage_HolidayType_Observance', + DatePage_HolidayType_Religious: 'DatePage_HolidayType_Religious', + + // ── DatePage: Cochrane Easter Egg (Interplanetary Telemetry) ── + DatePage_Telemetry_Title: 'DatePage_Telemetry_Title', + DatePage_Telemetry_Subtitle: 'DatePage_Telemetry_Subtitle', + DatePage_Telemetry_MoonDelay: 'DatePage_Telemetry_MoonDelay', + DatePage_Telemetry_MarsDelayMin: 'DatePage_Telemetry_MarsDelayMin', + DatePage_Telemetry_MarsDelayMax: 'DatePage_Telemetry_MarsDelayMax', + DatePage_Telemetry_MarsDelayCurrent: 'DatePage_Telemetry_MarsDelayCurrent', + DatePage_Telemetry_Unit: 'DatePage_Telemetry_Unit', + DatePage_Telemetry_Footer: 'DatePage_Telemetry_Footer', + // Navigation Nav_Home: 'Nav_Home', Nav_SoupDemo: 'Nav_SoupDemo', @@ -13,12 +62,14 @@ export const ShowcaseStrings = createI18nStringKeys(ShowcaseComponentId, { Nav_Blog: 'Nav_Blog', Nav_FAQ: 'Nav_FAQ', Nav_Docs: 'Nav_Docs', + Nav_BrightDate: 'Nav_BrightDate', Nav_Home_Description: 'Nav_Home_Description', Nav_SoupDemo_Description: 'Nav_SoupDemo_Description', Nav_Ledger_Description: 'Nav_Ledger_Description', Nav_Blog_Description: 'Nav_Blog_Description', Nav_FAQ_Description: 'Nav_FAQ_Description', Nav_Docs_Description: 'Nav_Docs_Description', + Nav_BrightDate_Description: 'Nav_BrightDate_Description', Nav_ToggleMenu: 'Nav_ToggleMenu', // Language Selector @@ -1693,10 +1744,14 @@ export const ShowcaseStrings = createI18nStringKeys(ShowcaseComponentId, { // Slogans Slogan_Math_Search_Warrant: 'Slogan_Math_Search_Warrant', - Slogan_Where_State_Sees_Noise: 'Slogan_Where_State_Sees_Noise', - Slogan_Building_World_Where_Ideas_Have_No_Borders: - 'Slogan_Building_World_Where_Ideas_Have_No_Borders', + Slogan_Signal_Belongs_To_You: 'Slogan_Signal_Belongs_To_You', Slogan_Defiance_By_Design: 'Slogan_Defiance_By_Design', + Slogan_BrightChain_Privacy: 'Slogan_BrightChain_Privacy', + Slogan_BrightChain_Privacy_Long: 'Slogan_BrightChain_Privacy_Long', + Slogan_Speak_Freely: 'Slogan_Speak_Freely', + Slogan_Distributed_By_Many: 'Slogan_Distributed_By_Many', + Slogan_Truth_In_The_Signal: 'Slogan_Truth_In_The_Signal', + Slogan_Ideas_Paper_Trail: 'Slogan_Ideas_Paper_Trail', } as const); export type ShowcaseStringKey = BrandedStringKeyValue; diff --git a/showcase/src/i18n/strings/englishUs.ts b/showcase/src/i18n/strings/englishUs.ts index c6a60a27..d1cbcb06 100644 --- a/showcase/src/i18n/strings/englishUs.ts +++ b/showcase/src/i18n/strings/englishUs.ts @@ -3,6 +3,67 @@ import { ShowcaseStringKey, ShowcaseStrings } from '../showcaseStrings'; export const ShowcaseAmericanEnglishStrings: ComponentStrings = { + // BrightChain + [ShowcaseStrings.Common_Bright]: 'Bright', + [ShowcaseStrings.Common_Chain]: 'Chain', + + [ShowcaseStrings.Common_Privacy]: 'Privacy', + [ShowcaseStrings.Common_Participation]: 'Participation', + [ShowcaseStrings.Common_Power]: 'Power', + [ShowcaseStrings.Common_PrivacyParticipationPower]: + 'Privacy. Participation. Power.', + + [ShowcaseStrings.Date_BrightDateTemplate]: 'BD {BD}', + + // ── DatePage ── + [ShowcaseStrings.DatePage_Title]: 'Date & Time', + [ShowcaseStrings.DatePage_BrightDateEpochLabel]: + 'BrightDate (decimal days since J2000.0)', + [ShowcaseStrings.DatePage_HolidaysTitle]: "Today's Holidays & Observances", + [ShowcaseStrings.DatePage_AllFormatsTitle]: 'All Date Formats', + [ShowcaseStrings.DatePage_AboutBrightDateTitle]: 'About BrightDate', + [ShowcaseStrings.DatePage_AboutBrightDate_Epoch]: + 'BrightDate counts decimal days since the J2000.0 epoch (January 1, 2000 at 12:00:00 UTC). This is the same epoch used by astronomers worldwide for celestial mechanics.', + [ShowcaseStrings.DatePage_AboutBrightDate_Fraction]: + 'The integer part is the day count. The fractional part is the decimal time of day. For example, 0.5 = noon, 0.25 = 06:00, 0.75 = 18:00.', + [ShowcaseStrings.DatePage_AboutBrightDate_NoTimezones]: + 'No time zones, no daylight saving, no ambiguity — just one number on one timeline.', + [ShowcaseStrings.DatePage_Format_BrightDateFull]: + 'BrightDate (full precision)', + [ShowcaseStrings.DatePage_Format_BrightDateCompact]: 'BrightDate (compact)', + [ShowcaseStrings.DatePage_Format_BrightDateStandard]: + 'BrightDate (standard)', + [ShowcaseStrings.DatePage_Format_ISO8601]: 'ISO 8601', + [ShowcaseStrings.DatePage_Format_UTC]: 'UTC', + [ShowcaseStrings.DatePage_Format_LocalDateTime]: 'Local Date & Time', + [ShowcaseStrings.DatePage_Format_LocalDate]: 'Local Date', + [ShowcaseStrings.DatePage_Format_LocalTime]: 'Local Time', + [ShowcaseStrings.DatePage_Format_UnixTimestamp]: 'Unix Timestamp', + [ShowcaseStrings.DatePage_Format_UnixMs]: 'Unix Milliseconds', + [ShowcaseStrings.DatePage_Format_JulianDate]: 'Julian Date', + [ShowcaseStrings.DatePage_Format_ModifiedJulianDate]: + 'Modified Julian Date', + [ShowcaseStrings.DatePage_Format_DayOfYear]: 'Day of Year', + [ShowcaseStrings.DatePage_Format_ISOWeek]: 'ISO Week', + [ShowcaseStrings.DatePage_Format_RFC2822]: 'RFC 2822', + [ShowcaseStrings.DatePage_HolidayType_Public]: 'Public Holiday', + [ShowcaseStrings.DatePage_HolidayType_Bank]: 'Bank Holiday', + [ShowcaseStrings.DatePage_HolidayType_Observance]: 'Observance', + [ShowcaseStrings.DatePage_HolidayType_Religious]: 'Religious', + + // ── DatePage: Cochrane Easter Egg (Interplanetary Telemetry) ── + [ShowcaseStrings.DatePage_Telemetry_Title]: 'Interplanetary Telemetry', + [ShowcaseStrings.DatePage_Telemetry_Subtitle]: + 'Current one-way light-delay expressed in millidays (md)', + [ShowcaseStrings.DatePage_Telemetry_MoonDelay]: 'Earth → Moon', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayMin]: 'Earth → Mars (min)', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayMax]: 'Earth → Mars (max)', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayCurrent]: + 'Earth → Mars (current est.)', + [ShowcaseStrings.DatePage_Telemetry_Unit]: 'md', + [ShowcaseStrings.DatePage_Telemetry_Footer]: + 'BrightDate is designed for a world beyond Earth-Standard Time. One milliday ≈ 86.4 seconds.', + // Navigation [ShowcaseStrings.Nav_Home]: 'Home', [ShowcaseStrings.Nav_SoupDemo]: 'Soup Demo', @@ -10,6 +71,7 @@ export const ShowcaseAmericanEnglishStrings: ComponentStrings [ShowcaseStrings.Nav_Blog]: 'Blog', [ShowcaseStrings.Nav_FAQ]: 'FAQ', [ShowcaseStrings.Nav_Docs]: 'Docs', + [ShowcaseStrings.Nav_BrightDate]: 'BrightDate', [ShowcaseStrings.Nav_Home_Description]: 'Main showcase page', [ShowcaseStrings.Nav_SoupDemo_Description]: 'Interactive block soup visualization', @@ -18,6 +80,7 @@ export const ShowcaseAmericanEnglishStrings: ComponentStrings [ShowcaseStrings.Nav_Blog_Description]: 'BrightChain blog and updates', [ShowcaseStrings.Nav_FAQ_Description]: 'Frequently asked questions', [ShowcaseStrings.Nav_Docs_Description]: 'Project documentation', + [ShowcaseStrings.Nav_BrightDate_Description]: 'Current BrightDate', [ShowcaseStrings.Nav_ToggleMenu]: 'Toggle menu', [ShowcaseStrings.FAQ_TopSecret_Logo_Alt]: 'Top Secret dApp', [ShowcaseStrings.FAQ_BrightChat_Logo_Alt]: 'BrightChat Logo', @@ -2381,9 +2444,19 @@ export const ShowcaseAmericanEnglishStrings: ComponentStrings [ShowcaseStrings.PP_S18_WebLabel]: 'Web:', [ShowcaseStrings.Slogan_Math_Search_Warrant]: 'Mathematics does not accept a search warrant.', - [ShowcaseStrings.Slogan_Where_State_Sees_Noise]: - 'Where the state sees noise, the people find truth.', - [ShowcaseStrings.Slogan_Building_World_Where_Ideas_Have_No_Borders]: - 'Building a world where ideas have no borders and truth has no owner.', + [ShowcaseStrings.Slogan_Signal_Belongs_To_You]: + 'The signal belongs to you. The noise belongs to everyone.', [ShowcaseStrings.Slogan_Defiance_By_Design]: 'Defiance by Design.', + [ShowcaseStrings.Slogan_BrightChain_Privacy]: + "Privacy you don't have to ask for.", + [ShowcaseStrings.Slogan_BrightChain_Privacy_Long]: + "BrightChain: Privacy you don't have to ask for.", + [ShowcaseStrings.Slogan_Speak_Freely]: + 'Speak freely; the math has your back.', + [ShowcaseStrings.Slogan_Distributed_By_Many]: + 'Distributed by many, owned by none.', + [ShowcaseStrings.Slogan_Truth_In_The_Signal]: + 'Truth in the signal, safety in the noise.', + [ShowcaseStrings.Slogan_Ideas_Paper_Trail]: + "Ideas shouldn't have a paper trail.", }; diff --git a/showcase/src/i18n/strings/french.ts b/showcase/src/i18n/strings/french.ts index ca5c8d3d..bd4560ca 100644 --- a/showcase/src/i18n/strings/french.ts +++ b/showcase/src/i18n/strings/french.ts @@ -5,6 +5,66 @@ import { ShowcaseStringKey, ShowcaseStrings } from '../showcaseStrings'; export const ShowcaseFrenchStrings: Partial< ComponentStrings > = { + // BrightChain + [ShowcaseStrings.Common_Bright]: 'Brillant', + [ShowcaseStrings.Common_Chain]: 'Chaîne', + + [ShowcaseStrings.Common_Privacy]: 'Confidentialité', + [ShowcaseStrings.Common_Participation]: 'Participation', + [ShowcaseStrings.Common_Power]: 'Pouvoir', + [ShowcaseStrings.Common_PrivacyParticipationPower]: + 'Confidentialité. Participation. Pouvoir.', + + [ShowcaseStrings.Date_BrightDateTemplate]: 'BD {BD}', + + // ── DatePage ── + [ShowcaseStrings.DatePage_Title]: 'Date et heure', + [ShowcaseStrings.DatePage_BrightDateEpochLabel]: + 'BrightDate (jours décimaux depuis J2000.0)', + [ShowcaseStrings.DatePage_HolidaysTitle]: 'Fêtes et célébrations du jour', + [ShowcaseStrings.DatePage_AllFormatsTitle]: 'Tous les formats de date', + [ShowcaseStrings.DatePage_AboutBrightDateTitle]: 'À propos de BrightDate', + [ShowcaseStrings.DatePage_AboutBrightDate_Epoch]: + "BrightDate compte les jours décimaux depuis l'époque J2000.0 (1er janvier 2000 à 12:00:00 UTC). C'est la même époque utilisée par les astronomes du monde entier pour la mécanique céleste.", + [ShowcaseStrings.DatePage_AboutBrightDate_Fraction]: + "La partie entière est le nombre de jours. La partie fractionnaire est l'heure décimale du jour. Par exemple, 0,5 = midi, 0,25 = 06:00, 0,75 = 18:00.", + [ShowcaseStrings.DatePage_AboutBrightDate_NoTimezones]: + "Pas de fuseaux horaires, pas d'heure d'été, pas d'ambiguïté — juste un nombre sur une ligne temporelle.", + [ShowcaseStrings.DatePage_Format_BrightDateFull]: + 'BrightDate (précision complète)', + [ShowcaseStrings.DatePage_Format_BrightDateCompact]: 'BrightDate (compact)', + [ShowcaseStrings.DatePage_Format_BrightDateStandard]: 'BrightDate (standard)', + [ShowcaseStrings.DatePage_Format_ISO8601]: 'ISO 8601', + [ShowcaseStrings.DatePage_Format_UTC]: 'UTC', + [ShowcaseStrings.DatePage_Format_LocalDateTime]: 'Date et heure locales', + [ShowcaseStrings.DatePage_Format_LocalDate]: 'Date locale', + [ShowcaseStrings.DatePage_Format_LocalTime]: 'Heure locale', + [ShowcaseStrings.DatePage_Format_UnixTimestamp]: 'Horodatage Unix', + [ShowcaseStrings.DatePage_Format_UnixMs]: 'Millisecondes Unix', + [ShowcaseStrings.DatePage_Format_JulianDate]: 'Date julienne', + [ShowcaseStrings.DatePage_Format_ModifiedJulianDate]: + 'Date julienne modifiée', + [ShowcaseStrings.DatePage_Format_DayOfYear]: "Jour de l'année", + [ShowcaseStrings.DatePage_Format_ISOWeek]: 'Semaine ISO', + [ShowcaseStrings.DatePage_Format_RFC2822]: 'RFC 2822', + [ShowcaseStrings.DatePage_HolidayType_Public]: 'Jour férié', + [ShowcaseStrings.DatePage_HolidayType_Bank]: 'Jour férié bancaire', + [ShowcaseStrings.DatePage_HolidayType_Observance]: 'Célébration', + [ShowcaseStrings.DatePage_HolidayType_Religious]: 'Religieux', + + // ── DatePage: Cochrane Easter Egg (Interplanetary Telemetry) ── + [ShowcaseStrings.DatePage_Telemetry_Title]: 'Télémétrie Interplanétaire', + [ShowcaseStrings.DatePage_Telemetry_Subtitle]: + 'Délai lumineux unidirectionnel actuel exprimé en millijours (md)', + [ShowcaseStrings.DatePage_Telemetry_MoonDelay]: 'Terre → Lune', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayMin]: 'Terre → Mars (min)', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayMax]: 'Terre → Mars (max)', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayCurrent]: + 'Terre → Mars (est. actuelle)', + [ShowcaseStrings.DatePage_Telemetry_Unit]: 'md', + [ShowcaseStrings.DatePage_Telemetry_Footer]: + 'BrightDate est conçu pour un monde au-delà du Temps Standard Terrestre. Un millijour ≈ 86,4 secondes.', + // Navigation [ShowcaseStrings.Nav_Home]: 'Accueil', [ShowcaseStrings.Nav_SoupDemo]: 'Démo Soup', @@ -12,6 +72,7 @@ export const ShowcaseFrenchStrings: Partial< [ShowcaseStrings.Nav_Blog]: 'Blog', [ShowcaseStrings.Nav_FAQ]: 'FAQ', [ShowcaseStrings.Nav_Docs]: 'Documentation', + [ShowcaseStrings.Nav_BrightDate]: 'BrightDate', [ShowcaseStrings.Nav_Home_Description]: 'Page principale', [ShowcaseStrings.Nav_SoupDemo_Description]: 'Visualisation interactive des blocs', @@ -20,6 +81,7 @@ export const ShowcaseFrenchStrings: Partial< [ShowcaseStrings.Nav_Blog_Description]: 'Blog et actualités BrightChain', [ShowcaseStrings.Nav_FAQ_Description]: 'Questions fréquemment posées', [ShowcaseStrings.Nav_Docs_Description]: 'Documentation du projet', + [ShowcaseStrings.Nav_BrightDate_Description]: 'Date BrightDate actuelle', [ShowcaseStrings.Nav_ToggleMenu]: 'Basculer le menu', // Language Selector @@ -2437,11 +2499,21 @@ export const ShowcaseFrenchStrings: Partial< [ShowcaseStrings.PP_S18_WebLabel]: 'Web :', [ShowcaseStrings.Slogan_Math_Search_Warrant]: "Les mathématiques n'acceptent pas de mandat de perquisition.", - [ShowcaseStrings.Slogan_Where_State_Sees_Noise]: - "Là où l'État voit du bruit, le peuple trouve la vérité.", - [ShowcaseStrings.Slogan_Building_World_Where_Ideas_Have_No_Borders]: - "Bâtir un monde où les idées n'ont pas de frontières et où la vérité n'a pas de propriétaire.", - [ShowcaseStrings.Slogan_Defiance_By_Design]: 'La Défiance par le Design.', + [ShowcaseStrings.Slogan_Signal_Belongs_To_You]: + 'Le signal vous appartient. Le bruit appartient à tous.', + [ShowcaseStrings.Slogan_Defiance_By_Design]: 'Résistance par conception.', + [ShowcaseStrings.Slogan_BrightChain_Privacy]: + "Une confidentialité que vous n'avez pas à demander.", + [ShowcaseStrings.Slogan_BrightChain_Privacy_Long]: + "BrightChain : Une confidentialité que vous n'avez pas à demander.", + [ShowcaseStrings.Slogan_Speak_Freely]: + 'Parlez librement ; les mathématiques vous protègent.', + [ShowcaseStrings.Slogan_Distributed_By_Many]: + 'Distribué par plusieurs, possédé par personne.', + [ShowcaseStrings.Slogan_Truth_In_The_Signal]: + 'La vérité dans le signal, la sécurité dans le bruit.', + [ShowcaseStrings.Slogan_Ideas_Paper_Trail]: + 'Les idées ne devraient pas laisser de traces.', }; export default ShowcaseFrenchStrings; diff --git a/showcase/src/i18n/strings/german.ts b/showcase/src/i18n/strings/german.ts index b5ddf2e7..d5b6ed48 100644 --- a/showcase/src/i18n/strings/german.ts +++ b/showcase/src/i18n/strings/german.ts @@ -5,6 +5,66 @@ import { ShowcaseStringKey, ShowcaseStrings } from '../showcaseStrings'; export const ShowcaseGermanStrings: Partial< ComponentStrings > = { + // BrightChain + [ShowcaseStrings.Common_Bright]: 'Hell', + [ShowcaseStrings.Common_Chain]: 'Kette', + + [ShowcaseStrings.Common_Privacy]: 'Privatsphäre', + [ShowcaseStrings.Common_Participation]: 'Teilhabe', + [ShowcaseStrings.Common_Power]: 'Macht', + [ShowcaseStrings.Common_PrivacyParticipationPower]: + 'Privatsphäre. Teilhabe. Macht.', + + [ShowcaseStrings.Date_BrightDateTemplate]: 'BD {BD}', + + // ── DatePage ── + [ShowcaseStrings.DatePage_Title]: 'Datum und Uhrzeit', + [ShowcaseStrings.DatePage_BrightDateEpochLabel]: + 'BrightDate (Dezimaltage seit J2000.0)', + [ShowcaseStrings.DatePage_HolidaysTitle]: 'Heutige Feiertage und Gedenktage', + [ShowcaseStrings.DatePage_AllFormatsTitle]: 'Alle Datumsformate', + [ShowcaseStrings.DatePage_AboutBrightDateTitle]: 'Über BrightDate', + [ShowcaseStrings.DatePage_AboutBrightDate_Epoch]: + 'BrightDate zählt Dezimaltage seit der J2000.0-Epoche (1. Januar 2000 um 12:00:00 UTC). Dies ist dieselbe Epoche, die von Astronomen weltweit für die Himmelsmechanik verwendet wird.', + [ShowcaseStrings.DatePage_AboutBrightDate_Fraction]: + 'Der ganzzahlige Teil ist die Tageszählung. Der Bruchteil ist die dezimale Tageszeit. Zum Beispiel: 0,5 = Mittag, 0,25 = 06:00, 0,75 = 18:00.', + [ShowcaseStrings.DatePage_AboutBrightDate_NoTimezones]: + 'Keine Zeitzonen, keine Sommerzeit, keine Mehrdeutigkeit — nur eine Zahl auf einer Zeitlinie.', + [ShowcaseStrings.DatePage_Format_BrightDateFull]: + 'BrightDate (volle Präzision)', + [ShowcaseStrings.DatePage_Format_BrightDateCompact]: 'BrightDate (kompakt)', + [ShowcaseStrings.DatePage_Format_BrightDateStandard]: 'BrightDate (Standard)', + [ShowcaseStrings.DatePage_Format_ISO8601]: 'ISO 8601', + [ShowcaseStrings.DatePage_Format_UTC]: 'UTC', + [ShowcaseStrings.DatePage_Format_LocalDateTime]: 'Lokales Datum und Uhrzeit', + [ShowcaseStrings.DatePage_Format_LocalDate]: 'Lokales Datum', + [ShowcaseStrings.DatePage_Format_LocalTime]: 'Lokale Uhrzeit', + [ShowcaseStrings.DatePage_Format_UnixTimestamp]: 'Unix-Zeitstempel', + [ShowcaseStrings.DatePage_Format_UnixMs]: 'Unix-Millisekunden', + [ShowcaseStrings.DatePage_Format_JulianDate]: 'Julianisches Datum', + [ShowcaseStrings.DatePage_Format_ModifiedJulianDate]: + 'Modifiziertes Julianisches Datum', + [ShowcaseStrings.DatePage_Format_DayOfYear]: 'Tag des Jahres', + [ShowcaseStrings.DatePage_Format_ISOWeek]: 'ISO-Woche', + [ShowcaseStrings.DatePage_Format_RFC2822]: 'RFC 2822', + [ShowcaseStrings.DatePage_HolidayType_Public]: 'Gesetzlicher Feiertag', + [ShowcaseStrings.DatePage_HolidayType_Bank]: 'Bankfeiertag', + [ShowcaseStrings.DatePage_HolidayType_Observance]: 'Gedenktag', + [ShowcaseStrings.DatePage_HolidayType_Religious]: 'Religiös', + + // ── DatePage: Cochrane Easter Egg (Interplanetary Telemetry) ── + [ShowcaseStrings.DatePage_Telemetry_Title]: 'Interplanetare Telemetrie', + [ShowcaseStrings.DatePage_Telemetry_Subtitle]: + 'Aktuelle Einweg-Lichtverzögerung in Millitagen (md)', + [ShowcaseStrings.DatePage_Telemetry_MoonDelay]: 'Erde → Mond', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayMin]: 'Erde → Mars (min)', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayMax]: 'Erde → Mars (max)', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayCurrent]: + 'Erde → Mars (aktuelle Schätzung)', + [ShowcaseStrings.DatePage_Telemetry_Unit]: 'md', + [ShowcaseStrings.DatePage_Telemetry_Footer]: + 'BrightDate ist für eine Welt jenseits der Erd-Standardzeit konzipiert. Ein Millitag ≈ 86,4 Sekunden.', + // Navigation [ShowcaseStrings.Nav_Home]: 'Startseite', [ShowcaseStrings.Nav_SoupDemo]: 'Soup Demo', @@ -12,6 +72,7 @@ export const ShowcaseGermanStrings: Partial< [ShowcaseStrings.Nav_Blog]: 'Blog', [ShowcaseStrings.Nav_FAQ]: 'FAQ', [ShowcaseStrings.Nav_Docs]: 'Dokumentation', + [ShowcaseStrings.Nav_BrightDate]: 'BrightDate', [ShowcaseStrings.Nav_Home_Description]: 'Hauptseite', [ShowcaseStrings.Nav_SoupDemo_Description]: 'Interaktive Block-Visualisierung', @@ -20,6 +81,7 @@ export const ShowcaseGermanStrings: Partial< [ShowcaseStrings.Nav_Blog_Description]: 'BrightChain Blog und Updates', [ShowcaseStrings.Nav_FAQ_Description]: 'Häufig gestellte Fragen', [ShowcaseStrings.Nav_Docs_Description]: 'Projektdokumentation', + [ShowcaseStrings.Nav_BrightDate_Description]: 'Aktuelles BrightDate', [ShowcaseStrings.Nav_ToggleMenu]: 'Menü umschalten', [ShowcaseStrings.FAQ_BrightDB_Logo_Alt]: 'BrightDB-Logo', [ShowcaseStrings.FAQ_TopSecret_Logo_Alt]: 'Top Secret dApp', @@ -2404,11 +2466,21 @@ export const ShowcaseGermanStrings: Partial< [ShowcaseStrings.PP_S18_WebLabel]: 'Web:', [ShowcaseStrings.Slogan_Math_Search_Warrant]: 'Mathematik akzeptiert keinen Durchsuchungsbefehl.', - [ShowcaseStrings.Slogan_Where_State_Sees_Noise]: - 'Wo der Staat nur Rauschen sieht, findet das Volk die Wahrheit.', - [ShowcaseStrings.Slogan_Building_World_Where_Ideas_Have_No_Borders]: - 'Wir bauen eine Welt, in der Ideen keine Grenzen kennen und die Wahrheit keinen Besitzer hat.', + [ShowcaseStrings.Slogan_Signal_Belongs_To_You]: + 'Das Signal gehört dir. Das Rauschen gehört allen.', [ShowcaseStrings.Slogan_Defiance_By_Design]: 'Widerstand durch Design.', + [ShowcaseStrings.Slogan_BrightChain_Privacy]: + 'Privatsphäre, um die du nicht bitten musst.', + [ShowcaseStrings.Slogan_BrightChain_Privacy_Long]: + 'BrightChain: Privatsphäre, um die du nicht bitten musst.', + [ShowcaseStrings.Slogan_Speak_Freely]: + 'Sprich frei; die Mathematik schützt dich.', + [ShowcaseStrings.Slogan_Distributed_By_Many]: + 'Von vielen verteilt, von niemandem besessen.', + [ShowcaseStrings.Slogan_Truth_In_The_Signal]: + 'Wahrheit im Signal, Sicherheit im Rauschen.', + [ShowcaseStrings.Slogan_Ideas_Paper_Trail]: + 'Ideen sollten keine Papierspur hinterlassen.', }; export default ShowcaseGermanStrings; diff --git a/showcase/src/i18n/strings/japanese.ts b/showcase/src/i18n/strings/japanese.ts index 44f730fe..05c9066f 100644 --- a/showcase/src/i18n/strings/japanese.ts +++ b/showcase/src/i18n/strings/japanese.ts @@ -5,6 +5,65 @@ import { ShowcaseStringKey, ShowcaseStrings } from '../showcaseStrings'; export const ShowcaseJapaneseStrings: Partial< ComponentStrings > = { + // BrightChain + [ShowcaseStrings.Common_Bright]: 'ブライト', + [ShowcaseStrings.Common_Chain]: 'チェーン', + + [ShowcaseStrings.Common_Privacy]: 'プライバシー', + [ShowcaseStrings.Common_Participation]: '参加', + [ShowcaseStrings.Common_Power]: '力', + [ShowcaseStrings.Common_PrivacyParticipationPower]: + 'プライバシー。参加。力。', + + [ShowcaseStrings.Date_BrightDateTemplate]: 'BD {BD}', + + // ── DatePage ── + [ShowcaseStrings.DatePage_Title]: '日付と時刻', + [ShowcaseStrings.DatePage_BrightDateEpochLabel]: + 'BrightDate(J2000.0からの10進日数)', + [ShowcaseStrings.DatePage_HolidaysTitle]: '本日の祝日・記念日', + [ShowcaseStrings.DatePage_AllFormatsTitle]: 'すべての日付形式', + [ShowcaseStrings.DatePage_AboutBrightDateTitle]: 'BrightDateについて', + [ShowcaseStrings.DatePage_AboutBrightDate_Epoch]: + 'BrightDateはJ2000.0エポック(2000年1月1日 12:00:00 UTC)からの10進日数を数えます。これは世界中の天文学者が天体力学に使用するのと同じエポックです。', + [ShowcaseStrings.DatePage_AboutBrightDate_Fraction]: + '整数部分は日数です。小数部分は1日の10進時刻です。例:0.5 = 正午、0.25 = 06:00、0.75 = 18:00。', + [ShowcaseStrings.DatePage_AboutBrightDate_NoTimezones]: + 'タイムゾーンなし、夏時間なし、曖昧さなし — 一つのタイムライン上の一つの数字だけ。', + [ShowcaseStrings.DatePage_Format_BrightDateFull]: 'BrightDate(完全精度)', + [ShowcaseStrings.DatePage_Format_BrightDateCompact]: + 'BrightDate(コンパクト)', + [ShowcaseStrings.DatePage_Format_BrightDateStandard]: 'BrightDate(標準)', + [ShowcaseStrings.DatePage_Format_ISO8601]: 'ISO 8601', + [ShowcaseStrings.DatePage_Format_UTC]: 'UTC', + [ShowcaseStrings.DatePage_Format_LocalDateTime]: 'ローカル日時', + [ShowcaseStrings.DatePage_Format_LocalDate]: 'ローカル日付', + [ShowcaseStrings.DatePage_Format_LocalTime]: 'ローカル時刻', + [ShowcaseStrings.DatePage_Format_UnixTimestamp]: 'Unixタイムスタンプ', + [ShowcaseStrings.DatePage_Format_UnixMs]: 'Unixミリ秒', + [ShowcaseStrings.DatePage_Format_JulianDate]: 'ユリウス日', + [ShowcaseStrings.DatePage_Format_ModifiedJulianDate]: '修正ユリウス日', + [ShowcaseStrings.DatePage_Format_DayOfYear]: '年間通算日', + [ShowcaseStrings.DatePage_Format_ISOWeek]: 'ISO週番号', + [ShowcaseStrings.DatePage_Format_RFC2822]: 'RFC 2822', + [ShowcaseStrings.DatePage_HolidayType_Public]: '祝日', + [ShowcaseStrings.DatePage_HolidayType_Bank]: '銀行休業日', + [ShowcaseStrings.DatePage_HolidayType_Observance]: '記念日', + [ShowcaseStrings.DatePage_HolidayType_Religious]: '宗教的', + + // ── DatePage: Cochrane Easter Egg (Interplanetary Telemetry) ── + [ShowcaseStrings.DatePage_Telemetry_Title]: '惑星間テレメトリー', + [ShowcaseStrings.DatePage_Telemetry_Subtitle]: + '現在の片道光遅延(ミリデイ単位、md)', + [ShowcaseStrings.DatePage_Telemetry_MoonDelay]: '地球 → 月', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayMin]: '地球 → 火星(最小)', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayMax]: '地球 → 火星(最大)', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayCurrent]: + '地球 → 火星(現在の推定)', + [ShowcaseStrings.DatePage_Telemetry_Unit]: 'md', + [ShowcaseStrings.DatePage_Telemetry_Footer]: + 'BrightDateは地球標準時を超えた世界のために設計されています。1ミリデイ ≈ 86.4秒。', + // Navigation [ShowcaseStrings.Nav_Home]: 'ホーム', [ShowcaseStrings.Nav_SoupDemo]: 'Soupデモ', @@ -12,6 +71,7 @@ export const ShowcaseJapaneseStrings: Partial< [ShowcaseStrings.Nav_Blog]: 'ブログ', [ShowcaseStrings.Nav_FAQ]: 'よくある質問', [ShowcaseStrings.Nav_Docs]: 'ドキュメント', + [ShowcaseStrings.Nav_BrightDate]: 'BrightDate', [ShowcaseStrings.Nav_Home_Description]: 'メインページ', [ShowcaseStrings.Nav_SoupDemo_Description]: 'インタラクティブなブロック可視化', @@ -20,6 +80,7 @@ export const ShowcaseJapaneseStrings: Partial< [ShowcaseStrings.Nav_Blog_Description]: 'BrightChainブログと更新情報', [ShowcaseStrings.Nav_FAQ_Description]: 'よくある質問', [ShowcaseStrings.Nav_Docs_Description]: 'プロジェクトドキュメント', + [ShowcaseStrings.Nav_BrightDate_Description]: '現在のBrightDate', [ShowcaseStrings.Nav_ToggleMenu]: 'メニュー切替', [ShowcaseStrings.FAQ_BrightDB_Logo_Alt]: 'BrightDBのロゴ', [ShowcaseStrings.FAQ_TopSecret_Logo_Alt]: 'トップシークレット dApp', @@ -2328,11 +2389,20 @@ export const ShowcaseJapaneseStrings: Partial< [ShowcaseStrings.PP_S18_WebLabel]: 'ウェブ:', [ShowcaseStrings.Slogan_Math_Search_Warrant]: '数学は捜索令状を受け付けない。', - [ShowcaseStrings.Slogan_Where_State_Sees_Noise]: - '国家がノイズを見る場所に、人々は真実を見出す。', - [ShowcaseStrings.Slogan_Building_World_Where_Ideas_Have_No_Borders]: - '思想に国境はなく、真実には所有者がいない世界を築く。', - [ShowcaseStrings.Slogan_Defiance_By_Design]: '設計による抵抗。', + [ShowcaseStrings.Slogan_Signal_Belongs_To_You]: + 'シグナルはあなたのもの。ノイズはみんなのもの。', + [ShowcaseStrings.Slogan_Defiance_By_Design]: '設計による反骨。', + [ShowcaseStrings.Slogan_BrightChain_Privacy]: + '許可を求めなくていいプライバシー。', + [ShowcaseStrings.Slogan_BrightChain_Privacy_Long]: + 'BrightChain:許可を求めなくていいプライバシー。', + [ShowcaseStrings.Slogan_Speak_Freely]: '自由に語れ。数学があなたを守る。', + [ShowcaseStrings.Slogan_Distributed_By_Many]: + '多くの人が配布し、誰も所有しない。', + [ShowcaseStrings.Slogan_Truth_In_The_Signal]: + 'シグナルに真実、ノイズに安全。', + [ShowcaseStrings.Slogan_Ideas_Paper_Trail]: + 'アイデアに痕跡を残すべきではない。', }; export default ShowcaseJapaneseStrings; diff --git a/showcase/src/i18n/strings/mandarin.ts b/showcase/src/i18n/strings/mandarin.ts index 8265f540..639a1fe3 100644 --- a/showcase/src/i18n/strings/mandarin.ts +++ b/showcase/src/i18n/strings/mandarin.ts @@ -5,6 +5,63 @@ import { ShowcaseStringKey, ShowcaseStrings } from '../showcaseStrings'; export const ShowcaseMandarinStrings: Partial< ComponentStrings > = { + // BrightChain + [ShowcaseStrings.Common_Bright]: '明亮', + [ShowcaseStrings.Common_Chain]: '链', + + [ShowcaseStrings.Common_Privacy]: '隐私', + [ShowcaseStrings.Common_Participation]: '参与', + [ShowcaseStrings.Common_Power]: '力量', + [ShowcaseStrings.Common_PrivacyParticipationPower]: '隐私。参与。力量。', + + [ShowcaseStrings.Date_BrightDateTemplate]: 'BD {BD}', + + // ── DatePage ── + [ShowcaseStrings.DatePage_Title]: '日期与时间', + [ShowcaseStrings.DatePage_BrightDateEpochLabel]: + 'BrightDate(自J2000.0以来的十进制天数)', + [ShowcaseStrings.DatePage_HolidaysTitle]: '今日节假日与纪念日', + [ShowcaseStrings.DatePage_AllFormatsTitle]: '所有日期格式', + [ShowcaseStrings.DatePage_AboutBrightDateTitle]: '关于BrightDate', + [ShowcaseStrings.DatePage_AboutBrightDate_Epoch]: + 'BrightDate计算自J2000.0纪元(2000年1月1日12:00:00 UTC)以来的十进制天数。这是全世界天文学家用于天体力学的同一纪元。', + [ShowcaseStrings.DatePage_AboutBrightDate_Fraction]: + '整数部分是天数计数。小数部分是一天中的十进制时间。例如:0.5 = 正午,0.25 = 06:00,0.75 = 18:00。', + [ShowcaseStrings.DatePage_AboutBrightDate_NoTimezones]: + '没有时区,没有夏令时,没有歧义——只是一条时间线上的一个数字。', + [ShowcaseStrings.DatePage_Format_BrightDateFull]: 'BrightDate(完整精度)', + [ShowcaseStrings.DatePage_Format_BrightDateCompact]: 'BrightDate(紧凑)', + [ShowcaseStrings.DatePage_Format_BrightDateStandard]: 'BrightDate(标准)', + [ShowcaseStrings.DatePage_Format_ISO8601]: 'ISO 8601', + [ShowcaseStrings.DatePage_Format_UTC]: 'UTC', + [ShowcaseStrings.DatePage_Format_LocalDateTime]: '本地日期和时间', + [ShowcaseStrings.DatePage_Format_LocalDate]: '本地日期', + [ShowcaseStrings.DatePage_Format_LocalTime]: '本地时间', + [ShowcaseStrings.DatePage_Format_UnixTimestamp]: 'Unix时间戳', + [ShowcaseStrings.DatePage_Format_UnixMs]: 'Unix毫秒', + [ShowcaseStrings.DatePage_Format_JulianDate]: '儒略日', + [ShowcaseStrings.DatePage_Format_ModifiedJulianDate]: '修正儒略日', + [ShowcaseStrings.DatePage_Format_DayOfYear]: '年中第几天', + [ShowcaseStrings.DatePage_Format_ISOWeek]: 'ISO周', + [ShowcaseStrings.DatePage_Format_RFC2822]: 'RFC 2822', + [ShowcaseStrings.DatePage_HolidayType_Public]: '公共假日', + [ShowcaseStrings.DatePage_HolidayType_Bank]: '银行假日', + [ShowcaseStrings.DatePage_HolidayType_Observance]: '纪念日', + [ShowcaseStrings.DatePage_HolidayType_Religious]: '宗教节日', + + // ── DatePage: Cochrane Easter Egg (Interplanetary Telemetry) ── + [ShowcaseStrings.DatePage_Telemetry_Title]: '行星际遥测', + [ShowcaseStrings.DatePage_Telemetry_Subtitle]: + '当前单向光延迟,以毫日(md)表示', + [ShowcaseStrings.DatePage_Telemetry_MoonDelay]: '地球 → 月球', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayMin]: '地球 → 火星(最小)', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayMax]: '地球 → 火星(最大)', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayCurrent]: + '地球 → 火星(当前估计)', + [ShowcaseStrings.DatePage_Telemetry_Unit]: 'md', + [ShowcaseStrings.DatePage_Telemetry_Footer]: + 'BrightDate专为超越地球标准时间的世界而设计。一毫日 ≈ 86.4秒。', + // Navigation [ShowcaseStrings.Nav_Home]: '首页', [ShowcaseStrings.Nav_SoupDemo]: 'Soup演示', @@ -12,12 +69,14 @@ export const ShowcaseMandarinStrings: Partial< [ShowcaseStrings.Nav_Blog]: '博客', [ShowcaseStrings.Nav_FAQ]: '常见问题', [ShowcaseStrings.Nav_Docs]: '文档', + [ShowcaseStrings.Nav_BrightDate]: 'BrightDate', [ShowcaseStrings.Nav_Home_Description]: '主页', [ShowcaseStrings.Nav_SoupDemo_Description]: '交互式区块可视化', [ShowcaseStrings.Nav_Ledger_Description]: '带治理的区块链账本', [ShowcaseStrings.Nav_Blog_Description]: 'BrightChain博客和更新', [ShowcaseStrings.Nav_FAQ_Description]: '常见问题解答', [ShowcaseStrings.Nav_Docs_Description]: '项目文档', + [ShowcaseStrings.Nav_BrightDate_Description]: '当前 BrightDate', [ShowcaseStrings.Nav_ToggleMenu]: '切换菜单', [ShowcaseStrings.FAQ_BrightDB_Logo_Alt]: 'BrightDB标志', [ShowcaseStrings.FAQ_TopSecret_Logo_Alt]: '绝密 dApp', @@ -2161,11 +2220,16 @@ export const ShowcaseMandarinStrings: Partial< [ShowcaseStrings.PP_S18_EmailLabel]: '电子邮件:', [ShowcaseStrings.PP_S18_WebLabel]: '网站:', [ShowcaseStrings.Slogan_Math_Search_Warrant]: '数学不接受搜查令。', - [ShowcaseStrings.Slogan_Where_State_Sees_Noise]: - '国家看到的只是噪音,人民发现的却是真理。', - [ShowcaseStrings.Slogan_Building_World_Where_Ideas_Have_No_Borders]: - '构建一个思想无国界、真理无主人的世界。', - [ShowcaseStrings.Slogan_Defiance_By_Design]: '设计即反抗。', + [ShowcaseStrings.Slogan_Signal_Belongs_To_You]: + '信号属于你。噪声属于所有人。', + [ShowcaseStrings.Slogan_Defiance_By_Design]: '以设计之名,坚守反抗。', + [ShowcaseStrings.Slogan_BrightChain_Privacy]: '无需申请的隐私保护。', + [ShowcaseStrings.Slogan_BrightChain_Privacy_Long]: + 'BrightChain:无需申请的隐私保护。', + [ShowcaseStrings.Slogan_Speak_Freely]: '自由发声;数学为你护航。', + [ShowcaseStrings.Slogan_Distributed_By_Many]: '众人分布,无人占有。', + [ShowcaseStrings.Slogan_Truth_In_The_Signal]: '信号中有真相,噪声中有安全。', + [ShowcaseStrings.Slogan_Ideas_Paper_Trail]: '思想不应留下痕迹。', }; export default ShowcaseMandarinStrings; diff --git a/showcase/src/i18n/strings/spanish.ts b/showcase/src/i18n/strings/spanish.ts index 389fa93f..6df9030b 100644 --- a/showcase/src/i18n/strings/spanish.ts +++ b/showcase/src/i18n/strings/spanish.ts @@ -4,6 +4,69 @@ import { ShowcaseStringKey, ShowcaseStrings } from '../showcaseStrings'; export const ShowcaseSpanishStrings: Partial< ComponentStrings > = { + // BrightChain + [ShowcaseStrings.Common_Bright]: 'Brillante', + [ShowcaseStrings.Common_Chain]: 'Cadena', + + [ShowcaseStrings.Common_Privacy]: 'Privacidad', + [ShowcaseStrings.Common_Participation]: 'Participación', + [ShowcaseStrings.Common_Power]: 'Poder', + [ShowcaseStrings.Common_PrivacyParticipationPower]: + 'Privacidad. Participación. Poder.', + + [ShowcaseStrings.Date_BrightDateTemplate]: 'BD {BD}', + + // ── DatePage ── + [ShowcaseStrings.DatePage_Title]: 'Fecha y hora', + [ShowcaseStrings.DatePage_BrightDateEpochLabel]: + 'BrightDate (días decimales desde J2000.0)', + [ShowcaseStrings.DatePage_HolidaysTitle]: + 'Festividades y celebraciones de hoy', + [ShowcaseStrings.DatePage_AllFormatsTitle]: 'Todos los formatos de fecha', + [ShowcaseStrings.DatePage_AboutBrightDateTitle]: 'Acerca de BrightDate', + [ShowcaseStrings.DatePage_AboutBrightDate_Epoch]: + 'BrightDate cuenta días decimales desde la época J2000.0 (1 de enero de 2000 a las 12:00:00 UTC). Es la misma época utilizada por astrónomos de todo el mundo para la mecánica celeste.', + [ShowcaseStrings.DatePage_AboutBrightDate_Fraction]: + 'La parte entera es el conteo de días. La parte fraccionaria es la hora decimal del día. Por ejemplo, 0,5 = mediodía, 0,25 = 06:00, 0,75 = 18:00.', + [ShowcaseStrings.DatePage_AboutBrightDate_NoTimezones]: + 'Sin zonas horarias, sin horario de verano, sin ambigüedad — solo un número en una línea temporal.', + [ShowcaseStrings.DatePage_Format_BrightDateFull]: + 'BrightDate (precisión completa)', + [ShowcaseStrings.DatePage_Format_BrightDateCompact]: + 'BrightDate (compacto)', + [ShowcaseStrings.DatePage_Format_BrightDateStandard]: + 'BrightDate (estándar)', + [ShowcaseStrings.DatePage_Format_ISO8601]: 'ISO 8601', + [ShowcaseStrings.DatePage_Format_UTC]: 'UTC', + [ShowcaseStrings.DatePage_Format_LocalDateTime]: 'Fecha y hora local', + [ShowcaseStrings.DatePage_Format_LocalDate]: 'Fecha local', + [ShowcaseStrings.DatePage_Format_LocalTime]: 'Hora local', + [ShowcaseStrings.DatePage_Format_UnixTimestamp]: 'Marca de tiempo Unix', + [ShowcaseStrings.DatePage_Format_UnixMs]: 'Milisegundos Unix', + [ShowcaseStrings.DatePage_Format_JulianDate]: 'Fecha juliana', + [ShowcaseStrings.DatePage_Format_ModifiedJulianDate]: + 'Fecha juliana modificada', + [ShowcaseStrings.DatePage_Format_DayOfYear]: 'Día del año', + [ShowcaseStrings.DatePage_Format_ISOWeek]: 'Semana ISO', + [ShowcaseStrings.DatePage_Format_RFC2822]: 'RFC 2822', + [ShowcaseStrings.DatePage_HolidayType_Public]: 'Día festivo', + [ShowcaseStrings.DatePage_HolidayType_Bank]: 'Día festivo bancario', + [ShowcaseStrings.DatePage_HolidayType_Observance]: 'Celebración', + [ShowcaseStrings.DatePage_HolidayType_Religious]: 'Religioso', + + // ── DatePage: Cochrane Easter Egg (Interplanetary Telemetry) ── + [ShowcaseStrings.DatePage_Telemetry_Title]: 'Telemetría Interplanetaria', + [ShowcaseStrings.DatePage_Telemetry_Subtitle]: + 'Retardo de luz unidireccional actual expresado en milidías (md)', + [ShowcaseStrings.DatePage_Telemetry_MoonDelay]: 'Tierra → Luna', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayMin]: 'Tierra → Marte (mín)', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayMax]: 'Tierra → Marte (máx)', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayCurrent]: + 'Tierra → Marte (est. actual)', + [ShowcaseStrings.DatePage_Telemetry_Unit]: 'md', + [ShowcaseStrings.DatePage_Telemetry_Footer]: + 'BrightDate está diseñado para un mundo más allá del Tiempo Estándar Terrestre. Un milidía ≈ 86,4 segundos.', + // Navigation [ShowcaseStrings.Nav_Home]: 'Inicio', [ShowcaseStrings.Nav_SoupDemo]: 'Demo Soup', @@ -11,6 +74,7 @@ export const ShowcaseSpanishStrings: Partial< [ShowcaseStrings.Nav_Blog]: 'Blog', [ShowcaseStrings.Nav_FAQ]: 'Preguntas Frecuentes', [ShowcaseStrings.Nav_Docs]: 'Documentación', + [ShowcaseStrings.Nav_BrightDate]: 'BrightDate', [ShowcaseStrings.Nav_Home_Description]: 'Página principal', [ShowcaseStrings.Nav_SoupDemo_Description]: 'Visualización interactiva de bloques', @@ -18,6 +82,7 @@ export const ShowcaseSpanishStrings: Partial< 'Libro mayor blockchain con gobernanza', [ShowcaseStrings.Nav_Blog_Description]: 'Blog y actualizaciones de BrightChain', + [ShowcaseStrings.Nav_BrightDate_Description]: 'BrightDate actual', [ShowcaseStrings.Nav_FAQ_Description]: 'Preguntas frecuentes', [ShowcaseStrings.Nav_Docs_Description]: 'Documentación del proyecto', [ShowcaseStrings.Nav_ToggleMenu]: 'Alternar menú', @@ -2435,11 +2500,21 @@ export const ShowcaseSpanishStrings: Partial< [ShowcaseStrings.PP_S18_WebLabel]: 'Web:', [ShowcaseStrings.Slogan_Math_Search_Warrant]: 'Las matemáticas no aceptan una orden de registro.', - [ShowcaseStrings.Slogan_Where_State_Sees_Noise]: - 'Donde el Estado ve ruido, el pueblo encuentra la verdad.', - [ShowcaseStrings.Slogan_Building_World_Where_Ideas_Have_No_Borders]: - 'Construyendo un mundo donde las ideas no tengan fronteras y la verdad no tenga dueño.', - [ShowcaseStrings.Slogan_Defiance_By_Design]: 'Desafío por Diseño.', + [ShowcaseStrings.Slogan_Signal_Belongs_To_You]: + 'La señal te pertenece. El ruido pertenece a todos.', + [ShowcaseStrings.Slogan_Defiance_By_Design]: 'Rebeldía por diseño.', + [ShowcaseStrings.Slogan_BrightChain_Privacy]: + 'Privacidad que no tienes que pedir.', + [ShowcaseStrings.Slogan_BrightChain_Privacy_Long]: + 'BrightChain: Privacidad que no tienes que pedir.', + [ShowcaseStrings.Slogan_Speak_Freely]: + 'Habla libremente; las matemáticas te respaldan.', + [ShowcaseStrings.Slogan_Distributed_By_Many]: + 'Distribuido por muchos, poseído por nadie.', + [ShowcaseStrings.Slogan_Truth_In_The_Signal]: + 'La verdad en la señal, la seguridad en el ruido.', + [ShowcaseStrings.Slogan_Ideas_Paper_Trail]: + 'Las ideas no deberían dejar rastro.', }; export default ShowcaseSpanishStrings; diff --git a/showcase/src/i18n/strings/ukrainian.ts b/showcase/src/i18n/strings/ukrainian.ts index 715c91d0..56eb73aa 100644 --- a/showcase/src/i18n/strings/ukrainian.ts +++ b/showcase/src/i18n/strings/ukrainian.ts @@ -5,6 +5,68 @@ import { ShowcaseStringKey, ShowcaseStrings } from '../showcaseStrings'; export const ShowcaseUkrainianStrings: Partial< ComponentStrings > = { + // BrightChain + [ShowcaseStrings.Common_Bright]: 'Яскравий', + [ShowcaseStrings.Common_Chain]: 'Ланцюг', + + [ShowcaseStrings.Common_Privacy]: 'Приватність', + [ShowcaseStrings.Common_Participation]: 'Участь', + [ShowcaseStrings.Common_Power]: 'Сила', + [ShowcaseStrings.Common_PrivacyParticipationPower]: + 'Приватність. Участь. Сила.', + + [ShowcaseStrings.Date_BrightDateTemplate]: 'BD {BD}', + + // ── DatePage ── + [ShowcaseStrings.DatePage_Title]: 'Дата та час', + [ShowcaseStrings.DatePage_BrightDateEpochLabel]: + 'BrightDate (десяткові дні від J2000.0)', + [ShowcaseStrings.DatePage_HolidaysTitle]: "Свята та пам'ятні дати сьогодні", + [ShowcaseStrings.DatePage_AllFormatsTitle]: 'Усі формати дати', + [ShowcaseStrings.DatePage_AboutBrightDateTitle]: 'Про BrightDate', + [ShowcaseStrings.DatePage_AboutBrightDate_Epoch]: + 'BrightDate рахує десяткові дні від епохи J2000.0 (1 січня 2000 року о 12:00:00 UTC). Це та сама епоха, яку використовують астрономи всього світу для небесної механіки.', + [ShowcaseStrings.DatePage_AboutBrightDate_Fraction]: + 'Ціла частина — це кількість днів. Дробова частина — це десятковий час доби. Наприклад: 0,5 = полудень, 0,25 = 06:00, 0,75 = 18:00.', + [ShowcaseStrings.DatePage_AboutBrightDate_NoTimezones]: + 'Без часових поясів, без літнього часу, без двозначності — лише одне число на одній часовій лінії.', + [ShowcaseStrings.DatePage_Format_BrightDateFull]: + 'BrightDate (повна точність)', + [ShowcaseStrings.DatePage_Format_BrightDateCompact]: + 'BrightDate (компактний)', + [ShowcaseStrings.DatePage_Format_BrightDateStandard]: + 'BrightDate (стандартний)', + [ShowcaseStrings.DatePage_Format_ISO8601]: 'ISO 8601', + [ShowcaseStrings.DatePage_Format_UTC]: 'UTC', + [ShowcaseStrings.DatePage_Format_LocalDateTime]: 'Місцева дата та час', + [ShowcaseStrings.DatePage_Format_LocalDate]: 'Місцева дата', + [ShowcaseStrings.DatePage_Format_LocalTime]: 'Місцевий час', + [ShowcaseStrings.DatePage_Format_UnixTimestamp]: 'Мітка часу Unix', + [ShowcaseStrings.DatePage_Format_UnixMs]: 'Мілісекунди Unix', + [ShowcaseStrings.DatePage_Format_JulianDate]: 'Юліанська дата', + [ShowcaseStrings.DatePage_Format_ModifiedJulianDate]: + 'Модифікована юліанська дата', + [ShowcaseStrings.DatePage_Format_DayOfYear]: 'День року', + [ShowcaseStrings.DatePage_Format_ISOWeek]: 'Тиждень ISO', + [ShowcaseStrings.DatePage_Format_RFC2822]: 'RFC 2822', + [ShowcaseStrings.DatePage_HolidayType_Public]: 'Державне свято', + [ShowcaseStrings.DatePage_HolidayType_Bank]: 'Банківське свято', + [ShowcaseStrings.DatePage_HolidayType_Observance]: "Пам'ятна дата", + [ShowcaseStrings.DatePage_HolidayType_Religious]: 'Релігійне', + + // ── DatePage: Cochrane Easter Egg (Interplanetary Telemetry) ── + [ShowcaseStrings.DatePage_Telemetry_Title]: 'Міжпланетна телеметрія', + [ShowcaseStrings.DatePage_Telemetry_Subtitle]: + 'Поточна одностороння світлова затримка у мілідобах (md)', + [ShowcaseStrings.DatePage_Telemetry_MoonDelay]: 'Земля → Місяць', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayMin]: 'Земля → Марс (мін)', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayMax]: 'Земля → Марс (макс)', + [ShowcaseStrings.DatePage_Telemetry_MarsDelayCurrent]: + 'Земля → Марс (поточна оцінка)', + [ShowcaseStrings.DatePage_Telemetry_Unit]: 'md', + [ShowcaseStrings.DatePage_Telemetry_Footer]: + 'BrightDate створено для світу за межами Земного Стандартного Часу. Один мілідень ≈ 86,4 секунди.', + // Navigation [ShowcaseStrings.Nav_Home]: 'Головна', [ShowcaseStrings.Nav_SoupDemo]: 'Демо Soup', @@ -12,6 +74,7 @@ export const ShowcaseUkrainianStrings: Partial< [ShowcaseStrings.Nav_Blog]: 'Блог', [ShowcaseStrings.Nav_FAQ]: 'Питання', [ShowcaseStrings.Nav_Docs]: 'Документація', + [ShowcaseStrings.Nav_BrightDate]: 'BrightDate', [ShowcaseStrings.Nav_Home_Description]: 'Головна сторінка', [ShowcaseStrings.Nav_SoupDemo_Description]: 'Інтерактивна візуалізація блоків', @@ -19,6 +82,7 @@ export const ShowcaseUkrainianStrings: Partial< [ShowcaseStrings.Nav_Blog_Description]: 'Блог та оновлення BrightChain', [ShowcaseStrings.Nav_FAQ_Description]: 'Часті запитання', [ShowcaseStrings.Nav_Docs_Description]: 'Документація проекту', + [ShowcaseStrings.Nav_BrightDate_Description]: 'Поточна BrightDate', [ShowcaseStrings.Nav_ToggleMenu]: 'Перемкнути меню', [ShowcaseStrings.FAQ_BrightDB_Logo_Alt]: 'Логотип BrightDB', [ShowcaseStrings.FAQ_TopSecret_Logo_Alt]: 'Цілком таємний dApp', @@ -2394,11 +2458,21 @@ export const ShowcaseUkrainianStrings: Partial< [ShowcaseStrings.PP_S18_WebLabel]: 'Веб:', [ShowcaseStrings.Slogan_Math_Search_Warrant]: 'Математика не приймає ордер на обшук.', - [ShowcaseStrings.Slogan_Where_State_Sees_Noise]: - 'Там, де держава бачить шум, народ знаходить правду.', - [ShowcaseStrings.Slogan_Building_World_Where_Ideas_Have_No_Borders]: - 'Будуємо світ, де ідеї не мають кордонів, а правда не має власника.', - [ShowcaseStrings.Slogan_Defiance_By_Design]: 'Спротив за задумом.', + [ShowcaseStrings.Slogan_Signal_Belongs_To_You]: + 'Сигнал належить тобі. Шум належить усім.', + [ShowcaseStrings.Slogan_Defiance_By_Design]: 'Спротив через дизайн.', + [ShowcaseStrings.Slogan_BrightChain_Privacy]: + 'Приватність, про яку не треба просити.', + [ShowcaseStrings.Slogan_BrightChain_Privacy_Long]: + 'BrightChain: Приватність, про яку не треба просити.', + [ShowcaseStrings.Slogan_Speak_Freely]: + 'Говори вільно; математика тебе захищає.', + [ShowcaseStrings.Slogan_Distributed_By_Many]: + 'Розподілено багатьма, не належить нікому.', + [ShowcaseStrings.Slogan_Truth_In_The_Signal]: + 'Правда в сигналі, безпека в шумі.', + [ShowcaseStrings.Slogan_Ideas_Paper_Trail]: + 'Ідеї не повинні залишати паперового сліду.', }; export default ShowcaseUkrainianStrings; diff --git a/showcase/src/shims/brightchain-react-components.ts b/showcase/src/shims/brightchain-react-components.ts index 493f233f..57237344 100644 --- a/showcase/src/shims/brightchain-react-components.ts +++ b/showcase/src/shims/brightchain-react-components.ts @@ -1,5 +1,7 @@ // Thin shim: only exports what the showcase app actually uses. // Avoids pulling in the full library index (which includes showcase demos, // identity wizards, and layout shells with Node.js-incompatible dependencies). +export { BrightChainLogoI18N } from '../../../brightchain-react-components/src/lib/BrightChainLogoI18N'; +export type { BrightChainLogoI18NProps } from '../../../brightchain-react-components/src/lib/BrightChainLogoI18N'; export { BrightChainSubLogo } from '../../../brightchain-react-components/src/lib/BrightChainSubLogo'; export type { BrightChainSubLogoProps } from '../../../brightchain-react-components/src/lib/BrightChainSubLogo'; diff --git a/tools/custom-jest-reporter.js b/tools/custom-jest-reporter.js new file mode 100644 index 00000000..612ae3d1 --- /dev/null +++ b/tools/custom-jest-reporter.js @@ -0,0 +1,96 @@ +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +// Shared progress file written by all parallel Jest processes, +// read by tools/scripts/test-with-progress.mjs for live ETA. +// Location: $BRIGHTCHAIN_PROGRESS_FILE env var, or a fixed tmp path. +const PROGRESS_FILE = + process.env.BRIGHTCHAIN_PROGRESS_FILE || + path.join(os.tmpdir(), 'brightchain-test-progress.jsonl'); + +/** + * Appends a single JSON line to the shared progress file. + * Multiple Jest processes write concurrently; lines are small enough + * to be atomic on local filesystems (well under PIPE_BUF / 4096 bytes). + */ +function emit(record) { + try { + fs.appendFileSync(PROGRESS_FILE, JSON.stringify(record) + '\n'); + } catch (_e) { + // Non-fatal — progress display degrades gracefully + } +} + +class CustomReporter { + constructor(globalConfig, options) { + this._globalConfig = globalConfig; + this._options = options; + } + + /** + * Fired once per Jest worker process when the run starts. + * numTotalTestSuites is the count of suite files this worker will run. + */ + onRunStart(results, _options) { + emit({ + event: 'run_start', + ts: Date.now(), + numTotalSuites: results.numTotalTestSuites, + }); + } + + /** + * Fired after every individual test case (Jest 27+). + * This is the high-resolution hook — one event per `it()`/`test()`. + * + * @param {import('@jest/types').Test.Test} test - the suite file + * @param {import('@jest/types').TestResult.AssertionResult} testCaseResult + */ + onTestCaseResult(test, testCaseResult) { + emit({ + event: 'test_case', + ts: Date.now(), + status: testCaseResult.status, // 'passed' | 'failed' | 'skipped' | 'todo' + duration: testCaseResult.duration ?? 0, // ms + fullName: testCaseResult.fullName, + ancestorTitles: testCaseResult.ancestorTitles, + suitePath: test.path, + // First failure message only (keep lines small) + failureMsg: testCaseResult.status === 'failed' + ? (testCaseResult.failureMessages[0] || '').split('\n')[0] + : undefined, + }); + } + + /** + * Fired after a whole suite file finishes. + * We use this to capture the aggregated totals Jest tracks internally, + * which gives the wrapper script a cross-check on the running counts. + */ + onTestResult(test, testResult, aggregatedResult) { + emit({ + event: 'suite_done', + ts: Date.now(), + suitePath: test.path, + suitePass: testResult.testResults.filter(t => t.status === 'passed').length, + suiteFail: testResult.testResults.filter(t => t.status === 'failed').length, + suiteSkip: testResult.testResults.filter(t => t.status === 'skipped' || t.status === 'todo').length, + // Jest's own running aggregate across all suites this process has seen + aggPassedTests: aggregatedResult.numPassedTests, + aggFailedTests: aggregatedResult.numFailedTests, + aggTotalTests: aggregatedResult.numTotalTests, + aggPassedSuites: aggregatedResult.numPassedTestSuites, + aggFailedSuites: aggregatedResult.numFailedTestSuites, + aggTotalSuites: aggregatedResult.numTotalTestSuites, + }); + } + + onRunComplete(_contexts, _results) { + emit({ event: 'run_complete', ts: Date.now() }); + } + + getLastError() {} +} + +module.exports = CustomReporter; diff --git a/tools/custom-jest-reporter.ts b/tools/custom-jest-reporter.ts new file mode 100644 index 00000000..3577f86e --- /dev/null +++ b/tools/custom-jest-reporter.ts @@ -0,0 +1,51 @@ + +import { + AggregatedResult, + Reporter, + TestContext, +} from '@jest/reporters'; + +class CustomReporter implements Reporter { + private _totalTests = 0; + private _testsCompleted = 0; + private _startTime = 0; + + onRunStart(results: AggregatedResult): void { + this._startTime = Date.now(); + this._totalTests = results.numTotalTests; + console.log(`Found ${this._totalTests} tests.`); + console.log('Starting test run...'); + } + + onTestResult(test: any, testResult: any, aggregatedResult: any): void { + this._testsCompleted += testResult.testResults.length; + const percentComplete = ((this._testsCompleted / this._totalTests) * 100).toFixed(2); + const elapsedTime = (Date.now() - this._startTime) / 1000; + const estimatedTime = (elapsedTime / this._testsCompleted) * (this._totalTests - this._testsCompleted); + + // Clear the console and print the progress + console.clear(); + console.log(`Test Progress: ${this._testsCompleted}/${this._totalTests} (${percentComplete}%)`); + console.log(`Elapsed Time: ${elapsedTime.toFixed(2)}s`); + if (isFinite(estimatedTime)) { + console.log(`Estimated Time Remaining: ${estimatedTime.toFixed(2)}s`); + } + } + + onRunComplete(contexts: Set, results: AggregatedResult): void { + const endTime = Date.now(); + const duration = (endTime - this._startTime) / 1000; + console.log(`\nTest run finished in ${duration.toFixed(2)}s.`); + if (results.numFailedTests > 0) { + console.error(`${results.numFailedTests} tests failed.`); + } else { + console.log('All tests passed!'); + } + } + + getLastError(): Error | void { + // This method is required, but we don't need to implement it for this use case. + } +} + +export default CustomReporter; diff --git a/tools/custom-playwright-reporter.js b/tools/custom-playwright-reporter.js new file mode 100644 index 00000000..195da0f0 --- /dev/null +++ b/tools/custom-playwright-reporter.js @@ -0,0 +1,71 @@ +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +// Shared progress file — read by tools/scripts/e2e-with-progress.mjs. +// Same env var convention as custom-jest-reporter.js so both can coexist. +const PROGRESS_FILE = + process.env.BRIGHTCHAIN_PROGRESS_FILE || + path.join(os.tmpdir(), 'brightchain-e2e-progress.jsonl'); + +function emit(record) { + try { + fs.appendFileSync(PROGRESS_FILE, JSON.stringify(record) + '\n'); + } catch { /* non-fatal */ } +} + +class CustomPlaywrightReporter { + constructor() { + this._totalTests = 0; + this._startTime = 0; + } + + /** + * Called once before any test runs. + * suite.allTests() gives the EXACT total — Playwright knows everything upfront. + */ + onBegin(config, suite) { + this._startTime = Date.now(); + this._totalTests = suite.allTests().length; + emit({ + event: 'run_start', + ts: this._startTime, + totalTests: this._totalTests, + // Project name from config if available + project: config.rootDir + ? path.basename(path.dirname(config.rootDir)) + : 'unknown', + }); + } + + /** + * Called after every individual test — the high-resolution hook. + */ + onTestEnd(test, result) { + emit({ + event: 'test_end', + ts: Date.now(), + status: result.status, // 'passed' | 'failed' | 'timedOut' | 'skipped' | 'interrupted' + duration: result.duration, // ms + title: test.title, + fullTitle: test.titlePath().join(' › '), + file: test.location?.file, + line: test.location?.line, + totalTests: this._totalTests, // repeat so wrapper always has it + failureMsg: result.status === 'failed' || result.status === 'timedOut' + ? (result.error?.message || '').split('\n')[0] + : undefined, + }); + } + + onEnd(result) { + emit({ + event: 'run_end', + ts: Date.now(), + status: result.status, + duration: Date.now() - this._startTime, + }); + } +} + +module.exports = CustomPlaywrightReporter; diff --git a/tools/scripts/count-tests.mjs b/tools/scripts/count-tests.mjs new file mode 100644 index 00000000..2b627478 --- /dev/null +++ b/tools/scripts/count-tests.mjs @@ -0,0 +1,289 @@ +#!/usr/bin/env node +/** + * count-tests.mjs + * + * Counts the total number of test cases across all nx projects by: + * 1. Running `nx run :test -- --listTests` in parallel (8 at a time) + * to get test file paths per project + * 2. Parsing each file with a regex to count it() / test() / it.each() calls + * + * Results are cached to .nx/test-count-cache.json (keyed by file mtime+size) + * so repeated runs are fast. Only changed files are re-counted. + * + * Usage: + * node tools/scripts/count-tests.mjs # count all (uses cache) + * node tools/scripts/count-tests.mjs --no-cache # force full recount + * node tools/scripts/count-tests.mjs --json # output JSON summary + * + * Also used by test-with-progress.mjs for pre-flight total estimation. + */ + +import { spawn, spawnSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; + +const ROOT = process.cwd(); +const CACHE_FILE = path.join(ROOT, '.nx', 'test-count-cache.json'); +const NO_CACHE = process.argv.includes('--no-cache'); +const JSON_OUTPUT = process.argv.includes('--json'); +const CONCURRENCY = 8; // parallel nx --listTests calls + +// ── load / save cache ───────────────────────────────────────────────────────── +function loadCache() { + if (NO_CACHE) return {}; + try { return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')); } catch { return {}; } +} + +function saveCache(cache) { + try { + fs.mkdirSync(path.dirname(CACHE_FILE), { recursive: true }); + fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2)); + } catch { /* non-fatal */ } +} + +// ── file fingerprint for cache key ─────────────────────────────────────────── +function fingerprint(filePath) { + try { + const s = fs.statSync(filePath); + return `${s.mtimeMs}:${s.size}`; + } catch { return null; } +} + +// ── count tests in a single file ───────────────────────────────────────────── +// Counts: it(), test(), it.each(), test.each(), and common variants. +// .each calls attempt to count data rows for a better estimate. +// Dynamic test generation (loops, factories) cannot be counted statically. + +function countEachRows(src, offset) { + let i = offset; + while (i < src.length && /\s/.test(src[i])) i++; + + if (src[i] === '`') { + // Tagged template table — count non-empty, non-header lines + const end = src.indexOf('`', i + 1); + if (end === -1) return 1; + const table = src.slice(i + 1, end); + const rows = table.split('\n').filter(l => l.trim() && !l.includes('$')); + return Math.max(1, rows.length); + } + + if (src[i] === '[') { + // Array of arrays or flat array — count top-level commas at depth 1 + let depth = 0, j = i; + while (j < src.length) { + if (src[j] === '[') depth++; + else if (src[j] === ']') { depth--; if (depth === 0) break; } + j++; + } + const arr = src.slice(i, j + 1); + let topCommas = 0, d = 0; + for (const ch of arr) { + if (ch === '[' || ch === '(') d++; + else if (ch === ']' || ch === ')') d--; + else if (ch === ',' && d === 1) topCommas++; + } + const innerArrays = (arr.match(/\[/g) || []).length - 1; + return innerArrays > 0 ? topCommas + 1 : 1; + } + + return 1; +} + +function countTestsInFile(filePath, cache) { + const fp = fingerprint(filePath); + if (fp && cache[filePath] && cache[filePath].fp === fp) { + return cache[filePath].count; + } + + let src; + try { src = fs.readFileSync(filePath, 'utf8'); } catch { return 0; } + + // Strip comments to avoid counting commented-out tests + const stripped = src + .replace(/\/\/[^\n]*/g, '') + .replace(/\/\*[\s\S]*?\*\//g, ''); + + let count = 0; + let m; + + // Count .each calls with row estimation + const eachRe = /\b(?:it|test)\.each\s*(?:<[^>]*>)?\s*\(/g; + while ((m = eachRe.exec(stripped)) !== null) { + const parenIdx = stripped.indexOf('(', m.index + m[0].length - 1); + count += countEachRows(stripped, parenIdx + 1); + } + + // Count regular (non-.each) test calls + const singleRe = /\b(?:it|test)(?:\.(?:only|skip|todo|concurrent|failing))?\s*\(/g; + while ((m = singleRe.exec(stripped)) !== null) { + const before = stripped.slice(Math.max(0, m.index - 6), m.index); + if (!before.includes('.each')) count++; + } + + if (fp) cache[filePath] = { fp, count }; + return count; +} + +// ── get test files for a project via nx --listTests (async) ────────────────── +function getTestFiles(projectName) { + // brightchain-api-e2e uses an 'e2e' target (Jest executor), not 'test' + const target = projectName === 'brightchain-api-e2e' ? 'e2e' : 'test'; + return new Promise((resolve) => { + const args = ['nx', 'run', `${projectName}:${target}`, '--', '--listTests']; + const child = spawn('yarn', args, { + cwd: ROOT, + encoding: 'utf8', + env: { ...process.env, NX_TUI: 'false' }, + }); + + let stdout = ''; + let stderr = ''; + child.stdout.on('data', d => { stdout += d; }); + child.stderr.on('data', d => { stderr += d; }); + + child.on('close', (code) => { + if (code !== 0) { resolve([]); return; } + const output = stdout + stderr; + const files = output + .split('\n') + .map(l => l.trim()) + .filter(l => l.startsWith('/') && /\.[jt]sx?$/.test(l)); + resolve(files); + }); + + // Timeout safety + setTimeout(() => { child.kill(); resolve([]); }, 30_000); + }); +} + +// ── parallel map with bounded concurrency ──────────────────────────────────── +async function parallelMap(items, fn, concurrency) { + const results = new Array(items.length); + let next = 0; + async function worker() { + while (next < items.length) { + const i = next++; + results[i] = await fn(items[i], i); + } + } + await Promise.all( + Array.from({ length: Math.min(concurrency, items.length) }, worker) + ); + return results; +} + +// ── get all projects with a test target (plus Jest-based e2e projects) ─────── +function getTestableProjects() { + const testResult = spawnSync( + 'yarn', + ['nx', 'show', 'projects', '--withTarget=test', '--json'], + { cwd: ROOT, encoding: 'utf8', env: { ...process.env, NX_TUI: 'false' }, timeout: 15_000 } + ); + const testProjects = testResult.status === 0 + ? (() => { try { return JSON.parse(testResult.stdout); } catch { return []; } })() + : []; + + // brightchain-api-e2e uses Jest (executor: @nx/jest:jest) under an 'e2e' target. + // --listTests hangs because the target has dependsOn: [brightchain-api:build]. + // We count its files directly instead of via nx --listTests. + const jestE2eProjects = []; // handled separately below via direct file scan + + return [...new Set([...testProjects, ...jestE2eProjects])]; +} + +// ── directly scan a directory for test files (no nx invocation) ────────────── +function scanTestFiles(dir) { + const results = []; + function walk(d) { + let entries; + try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; } + for (const e of entries) { + const full = path.join(d, e.name); + if (e.isDirectory() && e.name !== 'node_modules' && e.name !== 'dist') { + walk(full); + } else if (e.isFile() && /\.(spec|test)\.[jt]sx?$/.test(e.name)) { + results.push(full); + } + } + } + walk(dir); + return results; +} + +// ── main ────────────────────────────────────────────────────────────────────── +const cache = loadCache(); +const projects = getTestableProjects(); + +if (projects.length === 0) { + console.error('No testable projects found. Is this an nx workspace?'); + process.exit(1); +} + +// ── directly count Jest-based e2e projects (--listTests hangs due to dependsOn) ── +// These are counted by direct file scan and added to results separately. +// Also includes Playwright e2e projects — same reason (dependsOn build targets). +const JEST_E2E_DIRS = [ + { project: 'brightchain-api-e2e', dir: path.join(ROOT, 'brightchain-api-e2e/src') }, + { project: 'brightchain-react-e2e', dir: path.join(ROOT, 'brightchain-react-e2e/src') }, + { project: 'digitalburnbag-react-components', dir: path.join(ROOT, 'digitalburnbag-react-components/src/lib/__tests__/e2e') }, +]; + +const jestE2eResults = []; +for (const { project, dir } of JEST_E2E_DIRS) { + const files = scanTestFiles(dir); + let tests = 0; + for (const f of files) tests += countTestsInFile(f, cache); + jestE2eResults.push({ project, files: files.length, tests }); + if (!JSON_OUTPUT) { + console.log(` (direct) ${project.padEnd(42)} ${String(files.length).padStart(3)} files ${String(tests).padStart(6)} tests`); + } +} + +if (!JSON_OUTPUT) { + console.log(`\nCounting tests across ${projects.length} projects (${CONCURRENCY} parallel)...\n`); +} + +// Fetch all test file lists in parallel +const fileLists = await parallelMap(projects, async (project, i) => { + if (!JSON_OUTPUT) process.stdout.write(` [${String(i + 1).padStart(2)}/${projects.length}] ${project.padEnd(42)} listing...\r`); + const files = await getTestFiles(project); + if (!JSON_OUTPUT) process.stdout.write(` [${String(i + 1).padStart(2)}/${projects.length}] ${project.padEnd(42)} ${String(files.length).padStart(3)} files\n`); + return files; +}, CONCURRENCY); + +if (!JSON_OUTPUT) console.log(''); + +// Count tests in all files (sync, uses cache) +const results = [...jestE2eResults]; +let totalFiles = jestE2eResults.reduce((s, r) => s + r.files, 0); +let totalTests = jestE2eResults.reduce((s, r) => s + r.tests, 0); + +for (let i = 0; i < projects.length; i++) { + const project = projects[i]; + const files = fileLists[i]; + let projectTests = 0; + for (const f of files) { + projectTests += countTestsInFile(f, cache); + } + totalFiles += files.length; + totalTests += projectTests; + results.push({ project, files: files.length, tests: projectTests }); +} + +saveCache(cache); + +if (JSON_OUTPUT) { + console.log(JSON.stringify({ projects: results, totalFiles, totalTests }, null, 2)); +} else { + const line = '─'.repeat(65); + console.log(line); + for (const { project, files, tests } of results) { + if (files > 0) { + console.log(` ${project.padEnd(45)} ${String(files).padStart(4)} files ${String(tests).padStart(6)} tests`); + } + } + console.log(line); + console.log(` ${'TOTAL'.padEnd(45)} ${String(totalFiles).padStart(4)} files ${String(totalTests).padStart(6)} tests`); + console.log(`\nCached to ${CACHE_FILE}`); + console.log('Run "yarn test:count:refresh" to force a full recount.\n'); +} diff --git a/tools/scripts/e2e-with-progress.mjs b/tools/scripts/e2e-with-progress.mjs new file mode 100644 index 00000000..3183aed7 --- /dev/null +++ b/tools/scripts/e2e-with-progress.mjs @@ -0,0 +1,433 @@ +#!/usr/bin/env node +/** + * e2e-with-progress.mjs + * + * Runs the full e2e suite sequentially with a live ETA progress bar. + * Errors are written to e2e-errors.log in the workspace root. + * + * Projects run in order: + * 1. brightchain-api-e2e (Jest, ~348 tests) + * 2. brightchain-react-e2e (Playwright, ~368 tests) + * 3. digitalburnbag-react-components (Playwright, ~160 tests) + * ────────── + * Total: ~876 tests + * + * The Playwright total is reported exactly by the reporter at runtime + * (onBegin receives suite.allTests().length), so the ETA improves once + * each project starts. The Jest total comes from the test-count cache. + * + * Usage: + * yarn test:all:e2e:dev:progress + * yarn test:all:e2e:dev:progress --skip-nx-cache + */ + +import { spawn, spawnSync } from 'child_process'; +import { createInterface } from 'readline'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; + +const ROOT = process.cwd(); +const ERROR_LOG = path.join(ROOT, 'e2e-errors.log'); + +// ── shared progress file ────────────────────────────────────────────────────── +const PROGRESS_FILE = + process.env.BRIGHTCHAIN_PROGRESS_FILE || + path.join(os.tmpdir(), 'brightchain-e2e-progress.jsonl'); + +// ── error log helpers ───────────────────────────────────────────────────────── +let errorLogFd = null; + +function openErrorLog() { + try { + errorLogFd = fs.openSync(ERROR_LOG, 'w'); + const header = `E2E Error Log — ${new Date().toISOString()}\n${'─'.repeat(72)}\n\n`; + fs.writeSync(errorLogFd, header); + } catch { errorLogFd = null; } +} + +function logError(entry) { + if (!errorLogFd) return; + try { fs.writeSync(errorLogFd, entry); } catch { /* non-fatal */ } +} + +function closeErrorLog(hadErrors) { + if (!errorLogFd) return; + try { + if (!hadErrors) { + fs.writeSync(errorLogFd, 'No failures.\n'); + } + fs.closeSync(errorLogFd); + } catch { /* non-fatal */ } +} + +// ── ANSI helpers ────────────────────────────────────────────────────────────── +const C = { + reset: '\x1b[0m', + bold: '\x1b[1m', + cyan: '\x1b[36m', + green: '\x1b[32m', + red: '\x1b[31m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + grey: '\x1b[90m', + clearLine: '\x1b[2K\r', + up1: '\x1b[1A', +}; + +function bar(fraction, width = 36) { + const filled = Math.max(0, Math.min(width, Math.round(fraction * width))); + return `[${'█'.repeat(filled)}${'░'.repeat(width - filled)}]`; +} + +function fmt(seconds) { + if (!isFinite(seconds) || seconds < 0) return ' --:--'; + const m = Math.floor(seconds / 60); + const s = Math.floor(seconds % 60); + return `${String(m).padStart(2)}m${String(s).padStart(3)}s`; +} + +// ── state ───────────────────────────────────────────────────────────────────── +let grandTotal = 0; +let grandCompleted = 0; +let grandPassed = 0; +let grandFailed = 0; +let grandSkipped = 0; + +const projectTotals = {}; +const rateSamples = []; +const RATE_WINDOW = 100; + +// Full failure details for log file +const failures = []; // { project, title, file, line, msg, fullMsg } + +let currentProject = ''; +let lastTestTitle = ''; +let startTime = Date.now(); +let progressLineDrawn = false; +let lastFileSize = 0; +let lastLineBuffer = ''; + +// ── read new lines from the JSONL progress file ─────────────────────────────── +function readNewProgressLines() { + let stat; + try { stat = fs.statSync(PROGRESS_FILE); } catch { return; } + if (stat.size <= lastFileSize) return; + + const fd = fs.openSync(PROGRESS_FILE, 'r'); + const buf = Buffer.alloc(stat.size - lastFileSize); + fs.readSync(fd, buf, 0, buf.length, lastFileSize); + fs.closeSync(fd); + lastFileSize = stat.size; + + const chunk = lastLineBuffer + buf.toString('utf8'); + const lines = chunk.split('\n'); + lastLineBuffer = lines.pop(); + + for (const line of lines) { + if (!line.trim()) continue; + let rec; + try { rec = JSON.parse(line); } catch { continue; } + + switch (rec.event) { + case 'run_start': { + if (rec.totalTests > 0) { + const key = currentProject || rec.project || 'playwright'; + projectTotals[key] = rec.totalTests; + grandTotal = Object.values(projectTotals).reduce((a, b) => a + b, 0); + } + break; + } + + case 'test_end': { + if (rec.totalTests > 0) { + const key = currentProject || 'playwright'; + if (!projectTotals[key] || projectTotals[key] < rec.totalTests) { + projectTotals[key] = rec.totalTests; + grandTotal = Object.values(projectTotals).reduce((a, b) => a + b, 0); + } + } + const status = rec.status; + if (status === 'passed') { + grandPassed++; + } else if (status === 'failed' || status === 'timedOut') { + grandFailed++; + const failure = { + project: currentProject, + title: rec.fullTitle || rec.title, + file: rec.file, + line: rec.line, + msg: rec.failureMsg || '', + fullMsg: rec.failureMsg || '', + }; + failures.push(failure); + logError(formatFailureForLog(failure)); + } else { + grandSkipped++; + } + grandCompleted++; + if (rec.title) lastTestTitle = rec.title; + rateSamples.push({ t: rec.ts || Date.now(), n: grandCompleted }); + if (rateSamples.length > RATE_WINDOW) rateSamples.shift(); + break; + } + + case 'test_case': { + const status = rec.status; + if (status === 'passed') { + grandPassed++; + } else if (status === 'failed') { + grandFailed++; + const failure = { + project: currentProject, + title: rec.fullName, + file: rec.suitePath, + line: undefined, + msg: rec.failureMsg || '', + fullMsg: rec.failureMsg || '', + }; + failures.push(failure); + logError(formatFailureForLog(failure)); + } else { + grandSkipped++; + } + grandCompleted++; + if (rec.fullName) lastTestTitle = rec.fullName; + rateSamples.push({ t: rec.ts || Date.now(), n: grandCompleted }); + if (rateSamples.length > RATE_WINDOW) rateSamples.shift(); + break; + } + + case 'suite_done': { + if (rec.aggTotalTests) { + const key = currentProject || 'jest-e2e'; + const prev = projectTotals[key] || 0; + if (rec.aggTotalTests > prev) { + projectTotals[key] = rec.aggTotalTests; + grandTotal = Object.values(projectTotals).reduce((a, b) => a + b, 0); + } + } + break; + } + } + } +} + +function formatFailureForLog(f) { + const loc = f.file + ? `${path.relative(ROOT, f.file)}${f.line ? `:${f.line}` : ''}` + : ''; + const ts = new Date().toISOString(); + return [ + `[${ts}] [${f.project}] FAILED: ${f.title}`, + loc ? ` Location: ${loc}` : '', + f.fullMsg ? ` Error: ${f.fullMsg}` : '', + '', + ].filter(l => l !== undefined).join('\n') + '\n'; +} + +// ── ETA calculation ─────────────────────────────────────────────────────────── +function computeEta() { + if (grandCompleted === 0 || grandTotal === 0) return null; + if (grandCompleted >= grandTotal) return 0; + const remaining = grandTotal - grandCompleted; + if (rateSamples.length >= 2) { + const oldest = rateSamples[0]; + const newest = rateSamples[rateSamples.length - 1]; + const dt = newest.t - oldest.t; + const dn = newest.n - oldest.n; + if (dt > 0 && dn > 0) return (remaining / (dn / dt)) / 1000; + } + const elapsed = (Date.now() - startTime) / 1000; + return (elapsed / grandCompleted) * remaining; +} + +// ── render ──────────────────────────────────────────────────────────────────── +function redraw() { + readNewProgressLines(); + + const fraction = grandTotal > 0 ? Math.min(1, grandCompleted / grandTotal) : 0; + const elapsed = (Date.now() - startTime) / 1000; + const eta = computeEta(); + const pct = (fraction * 100).toFixed(1).padStart(5); + const totalStr = grandTotal > 0 ? String(grandTotal) : '?'; + const maxName = 40; + const shortName = lastTestTitle.length > maxName + ? '…' + lastTestTitle.slice(-(maxName - 1)) + : lastTestTitle; + + const line = [ + `${C.cyan}${C.bold}${bar(fraction)}${C.reset}`, + `${C.yellow}${pct}%${C.reset}`, + `${C.grey}${grandCompleted}/${totalStr}${C.reset}`, + `${C.blue}${fmt(elapsed)}${C.reset}`, + `${C.magenta}ETA ${eta !== null ? fmt(eta) : ' --:--'}${C.reset}`, + grandFailed > 0 + ? `${C.red}✖ ${grandFailed} failed${C.reset}` + : `${C.green}✔ ${grandPassed}${C.reset}`, + grandSkipped > 0 ? `${C.grey}⊘ ${grandSkipped}${C.reset}` : null, + currentProject ? `${C.grey}[${currentProject}]${C.reset}` : null, + shortName ? `${C.grey}${shortName}${C.reset}` : null, + ].filter(Boolean).join(' '); + + if (progressLineDrawn) process.stderr.write(C.up1 + C.clearLine); + process.stderr.write(line + '\n'); + progressLineDrawn = true; +} + +// ── run a single nx e2e project ─────────────────────────────────────────────── +function runProject(projectName, extraArgs = []) { + return new Promise((resolve) => { + currentProject = projectName; + progressLineDrawn = false; + + process.stderr.write(`\n${C.cyan}${C.bold}▶ ${projectName}${C.reset}\n\n`); + + const nxArgs = [ + 'nx', 'run', `${projectName}:e2e`, + '--configuration=development', + ...extraArgs, + ]; + + const child = spawn('yarn', nxArgs, { + cwd: ROOT, + env: { + ...process.env, + NODE_ENV: process.env.NODE_ENV || 'development', + BRIGHTCHAIN_PROGRESS_FILE: PROGRESS_FILE, + NX_TUI: 'false', + }, + stdio: ['inherit', 'pipe', 'pipe'], + }); + + const rlOut = createInterface({ input: child.stdout }); + rlOut.on('line', (line) => { + if (/^\s*[✔✖►]\s+nx run/.test(line)) return; + process.stdout.write(line + '\n'); + }); + + const rlErr = createInterface({ input: child.stderr }); + rlErr.on('line', (line) => { + if (/^\s*[✔✖►]\s+nx run/.test(line)) return; + process.stderr.write(line + '\n'); + }); + + child.on('close', (code) => resolve(code ?? 0)); + }); +} + +// ── load cached test totals for e2e projects ───────────────────────────────── +// pathFilter narrows to only files within a specific subdirectory, +// so digitalburnbag-react-components e2e files are not mixed with its unit tests. +function loadCachedTotal(projectName, pathFilter) { + const cacheFile = path.join(ROOT, '.nx', 'test-count-cache.json'); + try { + const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8')); + let total = 0; + for (const [filePath, entry] of Object.entries(cache)) { + if (filePath.includes(projectName) && + (!pathFilter || filePath.includes(pathFilter)) && + typeof entry.count === 'number') { + total += entry.count; + } + } + return total > 0 ? total : null; + } catch { return null; } +} + +// ── main ────────────────────────────────────────────────────────────────────── +const extraArgs = process.argv.slice(2); + +openErrorLog(); +try { fs.unlinkSync(PROGRESS_FILE); } catch { /* didn't exist */ } + +// Seed all three e2e project totals from cache so the progress bar has a +// realistic denominator from the very first test, not just after each project starts. +// Run `yarn test:count:refresh` to update these if test counts change significantly. +const E2E_PROJECTS = [ + { key: 'brightchain-api-e2e', pathFilter: 'brightchain-api-e2e/src' }, + { key: 'brightchain-react-e2e', pathFilter: 'brightchain-react-e2e/src' }, + { key: 'digitalburnbag-react-components', pathFilter: '__tests__/e2e' }, +]; + +let seededTotal = 0; +for (const { key, pathFilter } of E2E_PROJECTS) { + const n = loadCachedTotal(key, pathFilter); + if (n) { + projectTotals[key] = n; + grandTotal += n; + seededTotal += n; + process.stderr.write(`${C.grey}Pre-flight: ${key} ~${n} tests (cached)${C.reset}\n`); + } +} +if (seededTotal === 0) { + process.stderr.write(`${C.yellow}Pre-flight: no cache found — run 'yarn test:count:refresh' for better ETA${C.reset}\n`); +} else { + process.stderr.write(`${C.grey}Pre-flight: ~${seededTotal} total e2e tests expected${C.reset}\n`); +} +process.stderr.write(`${C.grey}Errors logged to: ${ERROR_LOG}${C.reset}\n`); + +startTime = Date.now(); +process.stderr.write(`\n${C.cyan}${C.bold}E2E Test Run — 3 projects (sequential)${C.reset}\n`); + +const pollInterval = setInterval(redraw, 200); +let exitCode = 0; + +// 1. brightchain-api-e2e (Jest) +const apiCode = await runProject('brightchain-api-e2e', ['--skip-nx-cache', ...extraArgs]); +if (apiCode !== 0) exitCode = apiCode; + +// 2. brightchain-react-e2e (Playwright, monitored config) +// --config passes the alternate playwright config to the @nx/playwright executor. +// --skip-nx-cache ensures the tests actually run rather than replaying cached results. +const reactCode = await runProject('brightchain-react-e2e', [ + '--config=brightchain-react-e2e/playwright.config.monitored.ts', + '--skip-nx-cache', + ...extraArgs, +]); +if (reactCode !== 0) exitCode = reactCode; + +// 3. digitalburnbag-react-components (Playwright, monitored config) +const burnbagCode = await runProject('digitalburnbag-react-components', [ + '--config=digitalburnbag-react-components/playwright.config.monitored.ts', + '--skip-nx-cache', + ...extraArgs, +]); +if (burnbagCode !== 0) exitCode = burnbagCode; + +clearInterval(pollInterval); +readNewProgressLines(); + +// ── final summary ───────────────────────────────────────────────────────────── +const duration = ((Date.now() - startTime) / 1000).toFixed(1); + +if (progressLineDrawn) process.stderr.write(C.up1 + C.clearLine); +const finalLine = [ + `${C.green}${C.bold}${bar(1)}${C.reset}`, + `${C.yellow}100.0%${C.reset}`, + `${C.grey}${grandCompleted}/${grandTotal || grandCompleted}${C.reset}`, + `${C.blue}${fmt(parseFloat(duration))}${C.reset}`, + `${C.magenta}ETA 0m 0s${C.reset}`, + grandFailed > 0 + ? `${C.red}✖ ${grandFailed} failed${C.reset}` + : `${C.green}✔ ${grandPassed} passed${C.reset}`, + grandSkipped > 0 ? `${C.grey}⊘ ${grandSkipped} skipped${C.reset}` : null, +].filter(Boolean).join(' '); +process.stderr.write(finalLine + '\n'); +process.stderr.write(`\n${C.cyan}🏁 E2E finished in ${duration}s${C.reset}\n`); + +if (failures.length > 0) { + process.stderr.write(`\n${C.red}${C.bold}${failures.length} test(s) failed — see ${ERROR_LOG}${C.reset}\n`); + // Print brief summary to terminal + for (const { project, title, msg } of failures) { + process.stderr.write(` ${C.red}✖ [${project}] ${title}${C.reset}\n`); + if (msg) process.stderr.write(` ${C.grey}${msg}${C.reset}\n`); + } +} else { + process.stderr.write(`${C.green}All e2e tests passed.${C.reset}\n`); +} + +closeErrorLog(failures.length > 0); +try { fs.unlinkSync(PROGRESS_FILE); } catch { /* already gone */ } +process.exit(exitCode); diff --git a/tools/scripts/test-with-progress.mjs b/tools/scripts/test-with-progress.mjs new file mode 100644 index 00000000..0d7763c8 --- /dev/null +++ b/tools/scripts/test-with-progress.mjs @@ -0,0 +1,420 @@ +#!/usr/bin/env node +/** + * test-with-progress.mjs + * + * Wraps `nx run-many --target=test` and renders a live ETA/progress bar + * driven by individual test-case completions (onTestCaseResult), giving + * much finer-grained rate data than per-suite or per-project events. + * + * Architecture: + * - tools/custom-jest-reporter.js → appends JSONL to a shared tmp file + * - this script → polls that file, renders progress bar + * + * Usage: + * yarn test:all:dev:16:progress + * yarn test:all:dev:16:progress --skip-nx-cache + */ + +import { spawn, spawnSync } from 'child_process'; +import { createInterface } from 'readline'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; + +const ROOT = process.cwd(); +const ERROR_LOG = path.join(ROOT, 'test-errors.log'); + +// ── error log helpers ───────────────────────────────────────────────────────── +let errorLogFd = null; + +function openErrorLog() { + try { + errorLogFd = fs.openSync(ERROR_LOG, 'w'); + const header = `Unit Test Error Log — ${new Date().toISOString()}\n${'─'.repeat(72)}\n\n`; + fs.writeSync(errorLogFd, header); + } catch { errorLogFd = null; } +} + +function logError(entry) { + if (!errorLogFd) return; + try { fs.writeSync(errorLogFd, entry); } catch { /* non-fatal */ } +} + +function closeErrorLog(hadErrors) { + if (!errorLogFd) return; + try { + if (!hadErrors) fs.writeSync(errorLogFd, 'No failures.\n'); + fs.closeSync(errorLogFd); + } catch { /* non-fatal */ } +} + +// ── shared progress file (must match custom-jest-reporter.js) ──────────────── +const PROGRESS_FILE = + process.env.BRIGHTCHAIN_PROGRESS_FILE || + path.join(os.tmpdir(), 'brightchain-test-progress.jsonl'); + +// ── ANSI helpers ───────────────────────────────────────────────────────────── +const C = { + reset: '\x1b[0m', + bold: '\x1b[1m', + cyan: '\x1b[36m', + green: '\x1b[32m', + red: '\x1b[31m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + grey: '\x1b[90m', + clearLine: '\x1b[2K\r', + up1: '\x1b[1A', +}; + +function bar(fraction, width = 32) { + const filled = Math.max(0, Math.min(width, Math.round(fraction * width))); + return `[${'█'.repeat(filled)}${'░'.repeat(width - filled)}]`; +} + +function fmt(seconds) { + if (!isFinite(seconds) || seconds < 0) return ' --:--'; + const m = Math.floor(seconds / 60); + const s = Math.floor(seconds % 60); + return `${String(m).padStart(2)}m${String(s).padStart(3)}s`; +} + +// ── state ───────────────────────────────────────────────────────────────────── +let totalProjects = 0; +let completedProjects = 0; + +// Test-level counters — driven by test_case events (one per it()/test()) +let passedTests = 0; +let failedTests = 0; +let skippedTests = 0; + +// knownTotal: best estimate of total tests. Grows as Jest discovers suites. +// We take the max of aggTotalTests seen across all suite_done events. +let knownTotal = 0; + +// Rate tracking: sliding window of (timestamp, completedCount) samples +const rateSamples = []; // { t: ms, n: completed } +const RATE_WINDOW = 80; // keep last N samples + +// Failure tracking: suitePath → [{ fullName, failureMsg }] +const failedCases = new Map(); + +// Last test name seen (for display) +let lastTestName = ''; + +let startTime = Date.now(); +let progressLineDrawn = false; +let lastFileSize = 0; +let lastLineBuffer = ''; + +// ── read new lines from the JSONL progress file ─────────────────────────────── +function readNewProgressLines() { + let stat; + try { stat = fs.statSync(PROGRESS_FILE); } catch { return; } + if (stat.size <= lastFileSize) return; + + const fd = fs.openSync(PROGRESS_FILE, 'r'); + const buf = Buffer.alloc(stat.size - lastFileSize); + fs.readSync(fd, buf, 0, buf.length, lastFileSize); + fs.closeSync(fd); + lastFileSize = stat.size; + + const chunk = lastLineBuffer + buf.toString('utf8'); + const lines = chunk.split('\n'); + lastLineBuffer = lines.pop(); // hold incomplete trailing line + + for (const line of lines) { + if (!line.trim()) continue; + let rec; + try { rec = JSON.parse(line); } catch { continue; } + + switch (rec.event) { + case 'test_case': { + const status = rec.status; + if (status === 'passed') { + passedTests++; + } else if (status === 'failed') { + failedTests++; + // Accumulate failures per suite + if (!failedCases.has(rec.suitePath)) failedCases.set(rec.suitePath, []); + failedCases.get(rec.suitePath).push({ + fullName: rec.fullName, + failureMsg: rec.failureMsg || '', + }); + // Write to error log immediately + const ts = new Date().toISOString(); + const shortPath = (rec.suitePath || '').split('/').slice(-3).join('/'); + logError(`[${ts}] FAILED: ${rec.fullName}\n Suite: ${shortPath}\n${rec.failureMsg ? ` Error: ${rec.failureMsg}\n` : ''}\n`); + } else { + skippedTests++; // 'skipped' | 'todo' | 'pending' + } + if (rec.fullName) lastTestName = rec.fullName; + + // Rate sample + const completed = passedTests + failedTests; + rateSamples.push({ t: rec.ts || Date.now(), n: completed }); + if (rateSamples.length > RATE_WINDOW) rateSamples.shift(); + break; + } + + case 'suite_done': { + // Use Jest's own aggregate to keep knownTotal up to date. + // aggTotalTests grows as Jest discovers and runs more suites. + if (rec.aggTotalTests > knownTotal) knownTotal = rec.aggTotalTests; + break; + } + + // run_start / run_complete — no action needed + } + } +} + +// ── ETA calculation ─────────────────────────────────────────────────────────── +function computeEta() { + const completed = passedTests + failedTests; + if (completed === 0) return null; + + // Effective total: use knownTotal if available, else extrapolate from + // project completion ratio (rough but better than nothing early on) + let effectiveTotal = knownTotal; + if (effectiveTotal === 0 && totalProjects > 0 && completedProjects > 0) { + // Extrapolate: assume completed projects are representative + effectiveTotal = Math.round((completed / completedProjects) * totalProjects); + } + if (effectiveTotal === 0 || completed >= effectiveTotal) return 0; + + const remaining = effectiveTotal - completed; + + // Sliding-window rate (tests/ms) + if (rateSamples.length >= 2) { + const oldest = rateSamples[0]; + const newest = rateSamples[rateSamples.length - 1]; + const dt = newest.t - oldest.t; + const dn = newest.n - oldest.n; + if (dt > 0 && dn > 0) { + return (remaining / (dn / dt)) / 1000; // seconds + } + } + + // Fallback: average rate from start + const elapsed = (Date.now() - startTime) / 1000; + return (elapsed / completed) * remaining; +} + +// ── render one progress line ────────────────────────────────────────────────── +function redraw() { + readNewProgressLines(); + + const completed = passedTests + failedTests; + const effectiveTotal = knownTotal || ( + totalProjects > 0 && completedProjects > 0 + ? Math.round((completed / completedProjects) * totalProjects) + : 0 + ); + const fraction = effectiveTotal > 0 ? Math.min(1, completed / effectiveTotal) : 0; + const elapsed = (Date.now() - startTime) / 1000; + const eta = computeEta(); + + const pct = (fraction * 100).toFixed(1).padStart(5); + const etaStr = eta !== null ? fmt(eta) : ' --:--'; + const elStr = fmt(elapsed); + const totalStr = effectiveTotal > 0 ? String(effectiveTotal) : '?'; + + // Truncate last test name to fit terminal + const maxName = 38; + const shortName = lastTestName.length > maxName + ? '…' + lastTestName.slice(-(maxName - 1)) + : lastTestName; + + const line = [ + `${C.cyan}${C.bold}${bar(fraction)}${C.reset}`, + `${C.yellow}${pct}%${C.reset}`, + `${C.grey}${completed}/${totalStr} tests${C.reset}`, + totalProjects > 0 + ? `${C.grey}${completedProjects}/${totalProjects} projects${C.reset}` + : null, + `${C.blue}${elStr}${C.reset}`, + `${C.magenta}ETA ${etaStr}${C.reset}`, + failedTests > 0 + ? `${C.red}✖ ${failedTests} failed${C.reset}` + : `${C.green}✔ ${passedTests}${C.reset}`, + shortName ? `${C.grey}${shortName}${C.reset}` : null, + ].filter(Boolean).join(' '); + + if (progressLineDrawn) { + process.stderr.write(C.up1 + C.clearLine); + } + process.stderr.write(line + '\n'); + progressLineDrawn = true; +} + +// ── load pre-counted total from cache if available ──────────────────────────── +function loadCachedTotal() { + const cacheFile = path.join(process.cwd(), '.nx', 'test-count-cache.json'); + try { + const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8')); + // Sum all cached file counts + let total = 0; + for (const entry of Object.values(cache)) { + if (entry && typeof entry.count === 'number') total += entry.count; + } + return total > 0 ? total : null; + } catch { return null; } +} + +// ── run the pre-flight counter (fast, uses cache) ───────────────────────────── +function preflight() { + process.stderr.write(`${C.grey}Running pre-flight test count (uses cache — run 'yarn test:count' to refresh)...${C.reset}\n`); + const result = spawnSync( + 'node', + ['tools/scripts/count-tests.mjs', '--json'], + { + cwd: process.cwd(), + encoding: 'utf8', + env: { ...process.env, NX_TUI: 'false' }, + timeout: 120_000, + } + ); + if (result.status === 0) { + try { + const data = JSON.parse(result.stdout); + return data.totalTests || null; + } catch { return null; } + } + return null; +} + +// ── clean up any stale progress file from a previous run ───────────────────── +try { fs.unlinkSync(PROGRESS_FILE); } catch { /* didn't exist */ } + +openErrorLog(); + +// ── get pre-counted total (from cache or fresh count) ──────────────────────── +const cachedTotal = loadCachedTotal(); +if (cachedTotal) { + knownTotal = cachedTotal; + process.stderr.write(`${C.grey}Pre-flight: ~${knownTotal} tests expected (from cache)${C.reset}\n`); +} else { + const counted = preflight(); + if (counted) { + knownTotal = counted; + process.stderr.write(`${C.grey}Pre-flight: ~${knownTotal} tests expected${C.reset}\n`); + } else { + process.stderr.write(`${C.grey}Pre-flight count unavailable — ETA will improve as tests run${C.reset}\n`); + } +} + +process.stderr.write(`${C.grey}Errors logged to: ${ERROR_LOG}${C.reset}\n`); + +// ── spawn nx ────────────────────────────────────────────────────────────────── +const extraArgs = process.argv.slice(2); + +const nxArgs = [ + 'nx', 'run-many', + '--target=test', + '--configuration=development', + '--parallel=16', + '--output-style=stream-without-prefixes', + ...extraArgs, +]; + +process.stderr.write( + `${C.cyan}▶ NODE_ENV=development yarn ${nxArgs.join(' ')}${C.reset}\n\n` +); + +const child = spawn('yarn', nxArgs, { + cwd: process.cwd(), + env: { + ...process.env, + NODE_ENV: process.env.NODE_ENV || 'development', + BRIGHTCHAIN_PROGRESS_FILE: PROGRESS_FILE, + NX_TUI: 'false', + }, + stdio: ['inherit', 'pipe', 'pipe'], +}); + +startTime = Date.now(); + +// ── parse nx stdout for project-level counts ────────────────────────────────── +const NX_TOTAL_RE = /Running target \S+ for (\d+) project/i; +const NX_DONE_RE = /^\s*[✔✖►]\s+nx run [\w-]+:test/; + +const rlOut = createInterface({ input: child.stdout }); +rlOut.on('line', (line) => { + const totalMatch = line.match(NX_TOTAL_RE); + if (totalMatch) { + totalProjects = parseInt(totalMatch[1], 10); + process.stderr.write( + `${C.cyan}🚀 Running tests across ${totalProjects} projects (parallel=16)...${C.reset}\n\n` + ); + progressLineDrawn = false; + return; + } + + if (NX_DONE_RE.test(line)) { + completedProjects++; + redraw(); + return; + } + + // Pass everything else through (Jest failure details, build errors, etc.) + process.stdout.write(line + '\n'); +}); + +const rlErr = createInterface({ input: child.stderr }); +rlErr.on('line', (line) => { + if (/^\s*[✔✖►]\s+nx run/.test(line)) return; + process.stderr.write(line + '\n'); +}); + +// ── poll every 150ms so the bar updates between project completions ─────────── +const pollInterval = setInterval(redraw, 150); + +// ── final summary ───────────────────────────────────────────────────────────── +child.on('close', (code) => { + clearInterval(pollInterval); + readNewProgressLines(); // drain any last writes + + const duration = ((Date.now() - startTime) / 1000).toFixed(1); + const completed = passedTests + failedTests; + + // Final bar at 100% + if (progressLineDrawn) process.stderr.write(C.up1 + C.clearLine); + const finalLine = [ + `${C.green}${C.bold}${bar(1)}${C.reset}`, + `${C.yellow}100.0%${C.reset}`, + `${C.grey}${completed}/${knownTotal || completed} tests${C.reset}`, + totalProjects > 0 + ? `${C.grey}${completedProjects}/${totalProjects} projects${C.reset}` + : null, + `${C.blue}${fmt(parseFloat(duration))}${C.reset}`, + `${C.magenta}ETA 0m 0s${C.reset}`, + failedTests > 0 + ? `${C.red}✖ ${failedTests} failed${C.reset}` + : `${C.green}✔ ${passedTests} passed${C.reset}`, + ].filter(Boolean).join(' '); + process.stderr.write(finalLine + '\n'); + + process.stderr.write(`\n${C.cyan}🏁 Finished in ${duration}s${C.reset}\n`); + + if (failedCases.size > 0) { + process.stderr.write(`\n${C.red}${C.bold}${failedTests} test(s) failed — see ${ERROR_LOG}${C.reset}\n`); + // Brief terminal summary + for (const [suitePath, cases] of failedCases) { + const shortPath = suitePath.split('/').slice(-3).join('/'); + process.stderr.write(`\n ${C.red}📄 ${shortPath}${C.reset}\n`); + for (const { fullName, failureMsg } of cases) { + process.stderr.write(` ${C.red}✖ ${fullName}${C.reset}\n`); + if (failureMsg) process.stderr.write(` ${C.grey}${failureMsg}${C.reset}\n`); + } + } + } else { + process.stderr.write(`${C.green}All tests passed.${C.reset}\n`); + } + + closeErrorLog(failedCases.size > 0); + try { fs.unlinkSync(PROGRESS_FILE); } catch { /* already gone */ } + + process.exit(code ?? 0); +}); diff --git a/tsconfig.base.json b/tsconfig.base.json index 77664d76..93b701ba 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -188,12 +188,6 @@ "@brightchain/brightledger-assets-react-components/*": [ "brightledger-assets-react-components/src/*" ], - "@digitaldefiance/brightdate": [ - "brightdate/src/index.ts" - ], - "@digitaldefiance/brightdate/*": [ - "brightdate/src/*" - ], "@brightchain/brightdate": [ "brightdate/src/index.ts" ], diff --git a/tsconfig.spec.json b/tsconfig.spec.json new file mode 100644 index 00000000..b4c53892 --- /dev/null +++ b/tsconfig.spec.json @@ -0,0 +1,24 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist/out-tsc", + "module": "ESNext", + "moduleResolution": "node", + "types": ["jest", "node"], + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": false, + "allowJs": true + }, + "include": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx" + ], + "exclude": [ + "node_modules", + "**/dist/**", + "tmp" + ] +} diff --git a/vscode-brightchain-vfs-explorer b/vscode-brightchain-vfs-explorer index 3d003d8e..73639a5b 160000 --- a/vscode-brightchain-vfs-explorer +++ b/vscode-brightchain-vfs-explorer @@ -1 +1 @@ -Subproject commit 3d003d8ed900e8888692cbfea7a8bbd36c443710 +Subproject commit 73639a5b5f78f95cf7788d21721c1e3f8eb3c7e5 diff --git a/yarn.lock b/yarn.lock index fa9007f3..61b75c5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2566,7 +2566,7 @@ __metadata: version: 0.0.0-use.local resolution: "@brightchain/brightchain-lib@workspace:brightchain-lib" dependencies: - "@brightchain/brightdate": "npm:0.32.0" + "@brightchain/brightdate": "npm:0.32.1" "@digitaldefiance/branded-interface": "npm:0.0.5" "@digitaldefiance/bzip2-wasm": "npm:^1.1.1" "@digitaldefiance/ecies-lib": "npm:5.2.2" @@ -2630,6 +2630,7 @@ __metadata: "@mui/utils": "npm:7.3.9" "@types/react": "npm:^19.2.14" "@types/react-dom": "npm:^19.2.3" + framer-motion: "npm:^12.38.0" react: "npm:19.2.4" react-router-dom: "npm:6.11.2" tslib: "npm:^2.3.0" @@ -2697,7 +2698,7 @@ __metadata: languageName: unknown linkType: soft -"@brightchain/brightdate@npm:0.32.0, @brightchain/brightdate@npm:^0.32.0, @brightchain/brightdate@workspace:brightdate": +"@brightchain/brightdate@npm:0.32.1, @brightchain/brightdate@npm:^0.32.1, @brightchain/brightdate@workspace:brightdate": version: 0.0.0-use.local resolution: "@brightchain/brightdate@workspace:brightdate" dependencies: @@ -2850,7 +2851,7 @@ __metadata: react-router-dom: "npm:^7.14.0" peerDependencies: "@brightchain/brightchain-lib": ^0.32.0 - "@brightchain/brightdate": ^0.32.0 + "@brightchain/brightdate": ^0.32.1 "@brightchain/brightmail-lib": ^0.32.0 "@digitaldefiance/express-suite-react-components": 4.29.1 "@mui/icons-material": "*" @@ -15214,7 +15215,7 @@ __metadata: "@brightchain/brightchain-lib": "npm:^0.32.0" "@brightchain/brightchain-react-components": "npm:^0.32.0" "@brightchain/brightchat-lib": "npm:^0.32.0" - "@brightchain/brightdate": "npm:^0.32.0" + "@brightchain/brightdate": "npm:^0.32.1" "@brightchain/brighthub-lib": "npm:^0.32.0" "@brightchain/brighthub-react-components": "npm:^0.32.0" "@brightchain/brightmail-lib": "npm:^0.32.0" @@ -19948,6 +19949,28 @@ __metadata: languageName: node linkType: hard +"framer-motion@npm:^12.38.0": + version: 12.38.0 + resolution: "framer-motion@npm:12.38.0" + dependencies: + motion-dom: "npm:^12.38.0" + motion-utils: "npm:^12.36.0" + tslib: "npm:^2.4.0" + peerDependencies: + "@emotion/is-prop-valid": "*" + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/is-prop-valid": + optional: true + react: + optional: true + react-dom: + optional: true + checksum: 10/4d529d1648a8e31ec9859e7ff1296b7e4ef0028eb09cbc7d626068766ab53e486038b431fac33b1438a1cc076a244e6843c5a8c0f38442885832308452b4b25e + languageName: node + linkType: hard + "fresh@npm:0.5.2, fresh@npm:~0.5.2": version: 0.5.2 resolution: "fresh@npm:0.5.2" @@ -25542,6 +25565,15 @@ __metadata: languageName: node linkType: hard +"motion-dom@npm:^12.38.0": + version: 12.38.0 + resolution: "motion-dom@npm:12.38.0" + dependencies: + motion-utils: "npm:^12.36.0" + checksum: 10/78c040b46d93273932cf80c70e39845be5a442dcaf18d4345b45a9193de9dfa87c885b609943cb652115e4eac5d46ef40b452185073dd43fc328b134f9975e90 + languageName: node + linkType: hard + "motion-utils@npm:^11.18.1": version: 11.18.1 resolution: "motion-utils@npm:11.18.1" @@ -25549,6 +25581,13 @@ __metadata: languageName: node linkType: hard +"motion-utils@npm:^12.36.0": + version: 12.36.0 + resolution: "motion-utils@npm:12.36.0" + checksum: 10/c4a2a7ffac48ca44082d6d31b115f245025060a7e69d70dac062646d8f96c39e5662a7c8a51f255566fdf8e719ef1269a8e9aa3a04fc263bb65b5a7b61331901 + languageName: node + linkType: hard + "mrmime@npm:^2.0.0": version: 2.0.1 resolution: "mrmime@npm:2.0.1"