diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 00000000000..45a18f26792 --- /dev/null +++ b/.markdownlint.jsonc @@ -0,0 +1,41 @@ +{ + // MD013/line-length - Line length + "MD013": false, + + // MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content + "MD024": { + "siblings_only": true + }, + + // MD032/blanks-around-lists - Lists should be surrounded by blank lines + "MD032": false, + + // MD033/no-inline-html - Inline HTML + "MD033": false, + + // MD041/first-line-heading/first-line-h1 - First line in a file should be a top-level heading + "MD041": false, + + // MD009/no-trailing-spaces - Trailing spaces + "MD009": false, + + // MD025/single-title/single-h1 - Multiple top-level headings in the same document + "MD025": false, + + // MD014/commands-show-output - Dollar signs used before commands without showing output + "MD014": false, + + // MD044/proper-names - Proper names should have the correct capitalization + "MD044": { + "code_blocks": false, + "names": [ + "Cake.Markdownlint", + "CommonMark", + "JavaScript", + "Markdown", + "markdown-it", + "markdownlint", + "Node.js" + ] + } +} diff --git a/apps/widget/cypress/e2e/branding.spec.ts b/apps/widget/cypress/e2e/branding.spec.ts index c87d9b131f0..0bfa1ee298a 100644 --- a/apps/widget/cypress/e2e/branding.spec.ts +++ b/apps/widget/cypress/e2e/branding.spec.ts @@ -38,8 +38,6 @@ describe('App Branding', function () { describe('App custom theme', function () { beforeEach(function () { - cy.intercept('**/widgets/organization').as('organizationSettings'); - const theme = { light: { layout: { @@ -48,28 +46,32 @@ describe('App custom theme', function () { }, }; - cy.initializeSession({ theme }) - .as('session') - .then((session: any) => { - cy.wait(500); - - return cy.task('createNotifications', { - identifier: session.templates[0].triggers[0].identifier, - token: session.token, - subscriberId: session.subscriber.subscriberId, - count: 5, - }); - }); + cy.initializeSession({ theme }).as('session'); }); it('should have branding applied', function () { - cy.wait('@organizationSettings'); - cy.wait(1000); - cy.getByTestId('layout-wrapper').should( 'have.css', 'background', 'rgb(255, 0, 0) none repeat scroll 0% 0% / auto padding-box border-box' ); + cy.getByTestId('notifications-header-title').should('contain', 'Notifications'); + }); +}); + +describe('App custom i18n', function () { + beforeEach(function () { + const i18n = { + lang: 'xyz', + translations: { + notifications: 'My custom notifications!', + }, + }; + + cy.initializeSession({ i18n }).as('session'); + }); + + it('should have custom language applied', function () { + cy.getByTestId('notifications-header-title').should('contain', 'My custom notifications!'); }); }); diff --git a/apps/widget/cypress/global.d.ts b/apps/widget/cypress/global.d.ts index 9dbb47ecd14..63dc1215263 100644 --- a/apps/widget/cypress/global.d.ts +++ b/apps/widget/cypress/global.d.ts @@ -1,6 +1,6 @@ /// -import { INovuThemeProvider } from '@novu/notification-center'; +import { INovuThemeProvider, ITranslationEntry } from '@novu/notification-center'; declare namespace Cypress { interface Chainable { @@ -32,7 +32,8 @@ declare namespace Cypress { session: any, shell?: boolean, encryptedHmacHash?: string, - theme?: INovuThemeProvider + theme?: INovuThemeProvider, + i18n?: ITranslationEntry ): Chainable; /** * Logs-in user by using API request @@ -42,6 +43,7 @@ declare namespace Cypress { shell?: boolean; hmacEncryption?: boolean; theme?: INovuThemeProvider; + i18n?: ITranslationEntry; }): Chainable; logout(): Chainable; diff --git a/apps/widget/cypress/support/commands.ts b/apps/widget/cypress/support/commands.ts index 9e58ab30982..6b468849489 100644 --- a/apps/widget/cypress/support/commands.ts +++ b/apps/widget/cypress/support/commands.ts @@ -82,11 +82,16 @@ Cypress.Commands.add('initializeSession', function (settings = {}) { ...session, subscriber, })) - : cy.initializeWidget({ session: session, encryptedHmacHash: encryptedHmacHash, theme: settings.theme }); + : cy.initializeWidget({ + session: session, + encryptedHmacHash: encryptedHmacHash, + theme: settings.theme, + i18n: settings.i18n, + }); }); }); -Cypress.Commands.add('initializeWidget', ({ session, encryptedHmacHash, theme }) => { +Cypress.Commands.add('initializeWidget', ({ session, encryptedHmacHash, theme, i18n }) => { const URL = `/${session.environment.identifier}`; return cy.visit(URL, { log: false }).then(() => cy @@ -109,6 +114,7 @@ Cypress.Commands.add('initializeWidget', ({ session, encryptedHmacHash, theme }) clientId: session.environment.identifier, data: user, theme, + i18n, }, }, }); diff --git a/apps/widget/src/components/notification-center/NotificationCenterWidget.tsx b/apps/widget/src/components/notification-center/NotificationCenterWidget.tsx index 69040f4fbbc..bd9c71c6fe7 100644 --- a/apps/widget/src/components/notification-center/NotificationCenterWidget.tsx +++ b/apps/widget/src/components/notification-center/NotificationCenterWidget.tsx @@ -1,4 +1,4 @@ -import { NotificationCenter, NovuProvider } from '@novu/notification-center'; +import { I18NLanguage, NotificationCenter, NovuProvider, ITranslationEntry } from '@novu/notification-center'; import { INovuThemeProvider } from '@novu/notification-center'; import { IMessage, IOrganizationEntity, ButtonTypeEnum } from '@novu/shared'; import { useEffect, useState } from 'react'; @@ -21,6 +21,7 @@ export function NotificationCenterWidget(props: INotificationCenterWidgetProps) const [theme, setTheme] = useState({}); const [fontFamily, setFontFamily] = useState('Lato'); const [frameInitialized, setFrameInitialized] = useState(false); + const [i18n, setI18n] = useState(); useEffect(() => { WebFont.load({ @@ -48,6 +49,10 @@ export function NotificationCenterWidget(props: INotificationCenterWidgetProps) setTheme(event.data.value.theme); } + if (event.data.value.i18n) { + setI18n(event.data.value.i18n); + } + setFrameInitialized(true); } }; @@ -79,6 +84,7 @@ export function NotificationCenterWidget(props: INotificationCenterWidgetProps) subscriberId={userDataPayload.subscriberId} onLoad={onLoad} subscriberHash={userDataPayload.subscriberHash} + i18n={i18n} > . Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). @@ -128,5 +128,5 @@ enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. +. Translations are available at +. diff --git a/docs/docs/notification-center/iframe-embed.md b/docs/docs/notification-center/iframe-embed.md index e5bcc36da41..66092ae8f74 100644 --- a/docs/docs/notification-center/iframe-embed.md +++ b/docs/docs/notification-center/iframe-embed.md @@ -3,6 +3,7 @@ If you are using an unsupported (yet) client framework, you can use our embed script, this will generate the notification center inside an iframe. You can find the embed code in the `Settings` page within the Admin Panel. It will look similar to this: + ```html ``` + Replace the selectors for the bell icon and the unseen badge withing your code. Let's take a look at this example code: ```html - + ``` ## Customizing the dropdown position @@ -40,16 +46,20 @@ Optionally the embed init script receives a position object, you can use it to s ```html ``` @@ -63,18 +73,50 @@ More information on all possible theme properties can be found [here](/notificat const customTheme = { light: { layout: { - background: 'red' - } + background: 'red', + }, + }, + }; + + novu.init( + '', + { + unseenBadgeSelector: '#unseen-badge', + bellSelector: '#notification-bell', + theme: customTheme, + }, + { + ...subscriberProps, } - } + ); + +``` - novu.init('', { - unseenBadgeSelector: '#unseen-badge', - bellSelector: '#notification-bell', - theme: customTheme - }, { - ...subscriberProps - }); +## Customizing the UI language + +The language of the UI can be customized by passing a `i18n` component to the init script. +More information on all possible properties for it can be found [here](/notification-center/react-components#customize-the-ui-language). + +```html + ``` @@ -87,37 +129,39 @@ Next step would be to generate an HMAC encrypted subscriberId on your backend: ```ts import { createHmac } from 'crypto'; -const hmacHash = createHmac('sha256', process.env.NOVU_API_KEY) - .update(subscriberId) - .digest('hex'); +const hmacHash = createHmac('sha256', process.env.NOVU_API_KEY).update(subscriberId).digest('hex'); ``` Then pass the created HMAC to your client side application forward it to the embed initialization script: ```ts -novu.init('', { - unseenBadgeSelector: '#unseen-badge', - bellSelector: '#notification-bell', - position: { - top: '50px', - left: '100px' +novu.init( + '', + { + unseenBadgeSelector: '#unseen-badge', + bellSelector: '#notification-bell', + position: { + top: '50px', + left: '100px', + }, + }, + { + subscriberId: 'REPLACE_WITH_PLAIN_VALUE', + subscriberHash: 'REPLACE_WITH_HASHED_VALUE', } -}, { - subscriberId: 'REPLACE_WITH_PLAIN_VALUE', - subscriberHash: 'REPLACE_WITH_HASHED_VALUE' -}) +); ``` ## Embed options parameters The second parameter of `novu.init` can be used to specify the options for the embed script. Here is a list of all the available options: -| Parameter | Type | Description | -| --------- | --------- |----------- | -| `bellSelector` | `string` | A `class` or `id` of the notification bell in your UI. We will attach an event listener for it. | -| `unseenBadgeSelector` | `string` | A selector to the unseen count badge (the red dot) which Novu can use to populate in case unseen notifications exist | -| `backendUrl` | `string` | Custom API location in case of self-hosted version of Novu | -| `socketUrl` | `string` | Custom WebSocket Service location in case of self-hosted version of Novu | -| `position.top` | `string` \| `number` | Override the top position of the notification center drop down | -| `position.left` | `string` \| `number` | Override the left position of the notification center drop down | -| `theme` | `object` | Provide a custom theme for the notification center to use (for example see [above](#customizing-the-theme)) | +| Parameter | Type | Description | +| --------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------- | +| `bellSelector` | `string` | A `class` or `id` of the notification bell in your UI. We will attach an event listener for it. | +| `unseenBadgeSelector` | `string` | A selector to the unseen count badge (the red dot) which Novu can use to populate in case unseen notifications exist | +| `backendUrl` | `string` | Custom API location in case of self-hosted version of Novu | +| `socketUrl` | `string` | Custom WebSocket Service location in case of self-hosted version of Novu | +| `position.top` | `string` \| `number` | Override the top position of the notification center drop down | +| `position.left` | `string` \| `number` | Override the left position of the notification center drop down | +| `theme` | `object` | Provide a custom theme for the notification center to use (for example see [above](#customizing-the-theme)) | diff --git a/libs/embed/src/embed.ts b/libs/embed/src/embed.ts index 0afa0f6507f..e2cd1fe356a 100644 --- a/libs/embed/src/embed.ts +++ b/libs/embed/src/embed.ts @@ -6,7 +6,7 @@ import iFrameResize from 'iframe-resizer'; import * as EventTypes from './shared/eventTypes'; import { UnmountedError, DomainVerificationError } from './shared/errors'; import { IFRAME_URL } from './shared/resources'; -import { INovuThemeProvider } from '@novu/notification-center'; +import { INovuThemeProvider, ITranslationEntry } from '@novu/notification-center'; const WEASL_WRAPPER_ID = 'novu-container'; const IFRAME_ID = 'novu-iframe-element'; @@ -20,6 +20,8 @@ class Novu { private theme?: INovuThemeProvider; + private i18n?: ITranslationEntry; + private debugMode: boolean; private onloadFunc: (b: any) => void; @@ -64,6 +66,7 @@ class Novu { this.backendUrl = selectorOrOptions.backendUrl; this.socketUrl = selectorOrOptions.socketUrl; this.theme = selectorOrOptions.theme; + this.i18n = selectorOrOptions.i18n; } this.clientId = clientId; @@ -266,6 +269,7 @@ class Novu { backendUrl: this.backendUrl, socketUrl: this.socketUrl, theme: this.theme, + i18n: this.i18n, topHost: window.location.host, data: options, }, @@ -356,6 +360,7 @@ interface IOptions { backendUrl?: string; socketUrl?: string; theme?: INovuThemeProvider; + i18n?: ITranslationEntry; position?: { top?: number | string; left?: number | string; diff --git a/package.json b/package.json index 9a952054565..c6b01010492 100644 --- a/package.json +++ b/package.json @@ -111,6 +111,7 @@ "lerna": "5.1.6", "lint-staged": "^10.5.4", "listr": "^0.14.3", + "markdownlint-cli": "^0.31.1", "meow": "^10.1.1", "mississippi": "^4.0.0", "pnpm": "7.5.0", @@ -155,7 +156,7 @@ ], "docs/**/*.{md,mdx}": [ "prettier --ignore-path ./.prettierignore --write", - "cd docs && npm run lint:md" + "markdownlint --ignore-path ./.gitignore --fix -c ./.markdownlint.jsonc" ], "libs/**/*.{ts,js,json}": [ "prettier --ignore-path ./.prettierignore --write", diff --git a/packages/notification-center/src/components/notification-center/components/layout/header/Header.tsx b/packages/notification-center/src/components/notification-center/components/layout/header/Header.tsx index 212f0d19235..57825ab74c5 100644 --- a/packages/notification-center/src/components/notification-center/components/layout/header/Header.tsx +++ b/packages/notification-center/src/components/notification-center/components/layout/header/Header.tsx @@ -16,7 +16,7 @@ export function Header({ unseenCount }: { unseenCount: number }) { return (
- {t('notifications')} + {t('notifications')} {!tabs && unseenCount && unseenCount > 0 ? (