Add missing translations (#2089)

* Add missing translations

* Add eslint-plugin-i18next plugin

* Add missing translation

* Update translations

* Update ESLint rules and improve UI text

* Update WellKnownURLs locales

* Add server-side translations in SetupLinkIndexPage
This commit is contained in:
Kiran K 2023-12-27 17:51:53 +05:30 committed by GitHub
parent 1525035092
commit fde4e59fa6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 239 additions and 113 deletions

View File

@ -9,18 +9,20 @@ module.exports = {
}, },
root: true, root: true,
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'], plugins: ['@typescript-eslint', 'i18next'],
extends: [ extends: [
'eslint:recommended', 'eslint:recommended',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'prettier', 'prettier',
'next/core-web-vitals', 'next/core-web-vitals',
'plugin:i18next/recommended',
], ],
overrides: [ overrides: [
{ {
files: ['*.ts', '*.tsx'], files: ['*.ts', '*.tsx'],
rules: { rules: {
'@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-explicit-any': 'off',
'import/no-anonymous-default-export': 'off',
}, },
}, },
{ {

View File

@ -1,8 +1,12 @@
import { useTranslation } from 'next-i18next';
export const PoweredBy = () => { export const PoweredBy = () => {
const { t } = useTranslation('common');
return ( return (
<p className='text-center text-xs text-gray-500 py-5'> <p className='text-center text-xs text-gray-500 py-5'>
<a href='https://boxyhq.com/' target='_blank' rel='noopener noreferrer'> <a href='https://boxyhq.com/' target='_blank' rel='noopener noreferrer'>
Powered by BoxyHQ {t('boxyhq_powered_by')}
</a> </a>
</p> </p>
); );

View File

@ -165,7 +165,7 @@ export const Sidebar = ({ isOpen, setIsOpen }: SidebarProps) => {
<div className='flex flex-shrink-0 items-center px-4'> <div className='flex flex-shrink-0 items-center px-4'>
<Link href='/' className='flex items-center'> <Link href='/' className='flex items-center'>
<Image src={Logo} alt='BoxyHQ' width={36} height={36} className='h-8 w-auto' /> <Image src={Logo} alt='BoxyHQ' width={36} height={36} className='h-8 w-auto' />
<span className='ml-4 text-xl font-bold text-gray-900'>BoxyHQ Admin Portal</span> <span className='ml-4 text-xl font-bold text-gray-900'>{t('boxyhq_admin_portal')}</span>
</Link> </Link>
</div> </div>
<div className='mt-5 h-0 flex-1 overflow-y-auto'> <div className='mt-5 h-0 flex-1 overflow-y-auto'>
@ -182,7 +182,7 @@ export const Sidebar = ({ isOpen, setIsOpen }: SidebarProps) => {
<div className='flex flex-shrink-0 items-center px-4'> <div className='flex flex-shrink-0 items-center px-4'>
<Link href='/' className='flex items-center'> <Link href='/' className='flex items-center'>
<Image src={Logo} alt='BoxyHQ' width={36} height={36} className='h-8 w-auto' /> <Image src={Logo} alt='BoxyHQ' width={36} height={36} className='h-8 w-auto' />
<span className='ml-4 text-lg font-bold text-gray-900'>BoxyHQ Admin Portal</span> <span className='ml-4 text-lg font-bold text-gray-900'>{t('boxyhq_admin_portal')}</span>
</Link> </Link>
</div> </div>
<div className='mt-5 flex flex-1 flex-col'> <div className='mt-5 flex flex-1 flex-col'>

View File

@ -13,49 +13,49 @@ const WellKnownURLs = () => {
const links = [ const links = [
{ {
title: 'SP Metadata', title: t('sp_metadata'),
description: t('sp_metadata_description'), description: t('sp_metadata_description'),
href: '/.well-known/sp-metadata', href: '/.well-known/sp-metadata',
buttonText: viewText, buttonText: viewText,
type: 'idp-config', type: 'idp-config',
}, },
{ {
title: 'SAML Configuration', title: t('saml_configuration'),
description: t('sp_config_description'), description: t('sp_config_description'),
href: '/.well-known/saml-configuration', href: '/.well-known/saml-configuration',
buttonText: viewText, buttonText: viewText,
type: 'idp-config', type: 'idp-config',
}, },
{ {
title: 'SAML Public Certificate', title: t('saml_public_cert'),
description: t('saml_public_cert_description'), description: t('saml_public_cert_description'),
href: '/.well-known/saml.cer', href: '/.well-known/saml.cer',
buttonText: downloadText, buttonText: downloadText,
type: 'idp-config', type: 'idp-config',
}, },
{ {
title: 'OpenID Configuration', title: t('oidc_configuration'),
description: t('oidc_config_description'), description: t('oidc_config_description'),
href: '/.well-known/oidc-configuration', href: '/.well-known/oidc-configuration',
buttonText: viewText, buttonText: viewText,
type: 'idp-config', type: 'idp-config',
}, },
{ {
title: 'OpenID Connect Discovery', title: t('oidc_discovery'),
description: t('oidc_discovery_description'), description: t('oidc_discovery_description'),
href: '/.well-known/openid-configuration', href: '/.well-known/openid-configuration',
buttonText: viewText, buttonText: viewText,
type: 'auth', type: 'auth',
}, },
{ {
title: 'IdP Metadata', title: t('idp_metadata'),
description: t('idp_metadata_description'), description: t('idp_metadata_description'),
href: '/.well-known/idp-metadata', href: '/.well-known/idp-metadata',
buttonText: viewText, buttonText: viewText,
type: 'saml-fed', type: 'saml-fed',
}, },
{ {
title: 'IdP Configuration', title: t('idp_configuration'),
description: t('idp_config_description'), description: t('idp_config_description'),
href: '/.well-known/idp-configuration', href: '/.well-known/idp-configuration',
buttonText: viewText, buttonText: viewText,
@ -74,23 +74,23 @@ const WellKnownURLs = () => {
<Tab <Tab
isActive={view === 'idp-config'} isActive={view === 'idp-config'}
setIsActive={() => setView('idp-config')} setIsActive={() => setView('idp-config')}
title='Identity Provider Configuration' title={t('idp_configuration_title')}
description='Links for SAML/OIDC IdP setup' description={t('idp_configuration_description')}
label='Identity Provider Configuration links' label={t('idp_configuration_label')}
/> />
<Tab <Tab
isActive={view === 'auth'} isActive={view === 'auth'}
setIsActive={() => setView('auth')} setIsActive={() => setView('auth')}
title='Auth integration' title={t('auth_integration_title')}
description='Links for OAuth 2.0/OpenID Connect auth' description={t('auth_integration_description')}
label='Auth integration links' label={t('auth_integration_label')}
/> />
<Tab <Tab
isActive={view === 'saml-fed'} isActive={view === 'saml-fed'}
setIsActive={() => setView('saml-fed')} setIsActive={() => setView('saml-fed')}
title='SAML Federation' title={t('saml_fed_configuration_title')}
description='Links for SAML Federation app setup' description={t('saml_fed_configuration_description')}
label='SAML Federation links' label={t('saml_fed_configuration_label')}
/> />
</div> </div>
<div className='space-y-3 mt-8'> <div className='space-y-3 mt-8'>

View File

@ -1,6 +1,7 @@
import Link from 'next/link'; import Link from 'next/link';
import type { Directory } from '@boxyhq/saml-jackson'; import type { Directory } from '@boxyhq/saml-jackson';
import classNames from 'classnames'; import classNames from 'classnames';
import { useTranslation } from 'next-i18next';
const DirectoryTab = ({ const DirectoryTab = ({
directory, directory,
@ -11,32 +12,34 @@ const DirectoryTab = ({
activeTab: string; activeTab: string;
setupLinkToken?: string; setupLinkToken?: string;
}) => { }) => {
const { t } = useTranslation('common');
const menus = setupLinkToken const menus = setupLinkToken
? [ ? [
{ {
name: 'Directory', name: t('directory'),
href: `/setup/${setupLinkToken}/directory-sync/${directory.id}`, href: `/setup/${setupLinkToken}/directory-sync/${directory.id}`,
active: activeTab === 'directory', active: activeTab === 'directory',
}, },
] ]
: [ : [
{ {
name: 'Directory', name: t('directory'),
href: `/admin/directory-sync/${directory.id}`, href: `/admin/directory-sync/${directory.id}`,
active: activeTab === 'directory', active: activeTab === 'directory',
}, },
{ {
name: 'Users', name: t('users'),
href: `/admin/directory-sync/${directory.id}/users`, href: `/admin/directory-sync/${directory.id}/users`,
active: activeTab === 'users', active: activeTab === 'users',
}, },
{ {
name: 'Groups', name: t('groups'),
href: `/admin/directory-sync/${directory.id}/groups`, href: `/admin/directory-sync/${directory.id}/groups`,
active: activeTab === 'groups', active: activeTab === 'groups',
}, },
{ {
name: 'Webhook Events', name: t('webhook_events'),
href: `/admin/directory-sync/${directory.id}/events`, href: `/admin/directory-sync/${directory.id}/events`,
active: activeTab === 'events', active: activeTab === 'events',
}, },

View File

@ -20,7 +20,7 @@ export const AccountLayout = ({ children }: { children: React.ReactNode }) => {
return ( return (
<> <>
<Head> <Head>
<title>Admin Portal | BoxyHQ</title> <title>{t('boxyhq_admin_portal')}</title>
<link rel='icon' href='/favicon.ico' /> <link rel='icon' href='/favicon.ico' />
</Head> </Head>
<Sidebar isOpen={isOpen} setIsOpen={setIsOpen} /> <Sidebar isOpen={isOpen} setIsOpen={setIsOpen} />

View File

@ -3,9 +3,11 @@ import classNames from 'classnames';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { successToast, errorToast } from '@components/Toaster'; import { successToast, errorToast } from '@components/Toaster';
import { LinkBack } from '@components/LinkBack'; import { LinkBack } from '@components/LinkBack';
import { useTranslation } from 'next-i18next';
const AddProject = () => { const AddProject = () => {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation('common');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [project, setProject] = useState({ const [project, setProject] = useState({
@ -49,7 +51,7 @@ const AddProject = () => {
} }
if (data && data.project) { if (data && data.project) {
successToast('Project created successfully.'); successToast(t('retraced_project_created'));
router.replace(`/admin/retraced/projects/${data.project.id}`); router.replace(`/admin/retraced/projects/${data.project.id}`);
} }
}; };
@ -58,13 +60,15 @@ const AddProject = () => {
<> <>
<LinkBack href='/admin/retraced/projects' /> <LinkBack href='/admin/retraced/projects' />
<div className='mt-5'> <div className='mt-5'>
<h2 className='mb-5 mt-5 font-bold text-gray-700 dark:text-white md:text-xl'>Create Project</h2> <h2 className='mb-5 mt-5 font-bold text-gray-700 dark:text-white md:text-xl'>
{t('create_project')}
</h2>
<div className='min-w-[28rem] rounded border border-gray-200 bg-white p-3 dark:border-gray-700 dark:bg-gray-800 md:w-3/4 md:max-w-lg'> <div className='min-w-[28rem] rounded border border-gray-200 bg-white p-3 dark:border-gray-700 dark:bg-gray-800 md:w-3/4 md:max-w-lg'>
<form onSubmit={createProject}> <form onSubmit={createProject}>
<div className='flex flex-col space-y-3'> <div className='flex flex-col space-y-3'>
<div className='form-control w-full'> <div className='form-control w-full'>
<label className='label'> <label className='label'>
<span className='label-text'>Project name</span> <span className='label-text'>{t('project_name')}</span>
</label> </label>
<input <input
type='text' type='text'
@ -76,7 +80,7 @@ const AddProject = () => {
</div> </div>
<div> <div>
<button className={classNames('btn-primary btn', loading ? 'loading' : '')}> <button className={classNames('btn-primary btn', loading ? 'loading' : '')}>
Create Project {t('create_project')}
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
import RetracedEventsBrowser from '@retracedhq/logs-viewer'; import RetracedEventsBrowser from '@retracedhq/logs-viewer';
import useSWR from 'swr'; import useSWR from 'swr';
import { useTranslation } from 'next-i18next';
import type { ApiError, ApiSuccess } from 'types'; import type { ApiError, ApiSuccess } from 'types';
import type { Project } from 'types/retraced'; import type { Project } from 'types/retraced';
@ -8,6 +9,8 @@ import Loading from '@components/Loading';
import { fetcher } from '@lib/ui/utils'; import { fetcher } from '@lib/ui/utils';
const LogsViewer = (props: { project: Project; environmentId: string; groupId: string; host: string }) => { const LogsViewer = (props: { project: Project; environmentId: string; groupId: string; host: string }) => {
const { t } = useTranslation('common');
const { project, environmentId, groupId, host } = props; const { project, environmentId, groupId, host } = props;
const token = project.tokens.filter((token) => token.environment_id === environmentId)[0]; const token = project.tokens.filter((token) => token.environment_id === environmentId)[0];
@ -36,7 +39,7 @@ const LogsViewer = (props: { project: Project; environmentId: string; groupId: s
<RetracedEventsBrowser <RetracedEventsBrowser
host={`${host}/viewer/v1`} host={`${host}/viewer/v1`}
auditLogToken={viewerToken} auditLogToken={viewerToken}
header='Audit Logs' header={t('audit_logs')}
customClass={'text-primary dark:text-white'} customClass={'text-primary dark:text-white'}
skipViewLogEvent={true} skipViewLogEvent={true}
/> />

View File

@ -19,7 +19,7 @@ const ProjectDetails = (props: { project: Project; host?: string }) => {
<> <>
<div className='form-control mb-5 max-w-xs'> <div className='form-control mb-5 max-w-xs'>
<label className='label pl-0'> <label className='label pl-0'>
<span className='label-text'>Environment</span> <span className='label-text'>{t('environment')}</span>
</label> </label>
<Select <Select
value={selectedIndex} value={selectedIndex}

View File

@ -1,9 +1,10 @@
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { ButtonPrimary } from '@components/ButtonPrimary'; import { ButtonPrimary } from '@components/ButtonPrimary';
const NextButton = () => { const NextButton = () => {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation('common');
const onClick = () => { const onClick = () => {
const { idp, step, token } = router.query as { idp: string; step: string; token: string }; const { idp, step, token } = router.query as { idp: string; step: string; token: string };
@ -20,7 +21,7 @@ const NextButton = () => {
return ( return (
<div> <div>
<ButtonPrimary onClick={onClick}>Next Step</ButtonPrimary> <ButtonPrimary onClick={onClick}>{t('next_step')}</ButtonPrimary>
</div> </div>
); );
}; };

View File

@ -1,9 +1,10 @@
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { ButtonOutline } from '@components/ButtonOutline'; import { ButtonOutline } from '@components/ButtonOutline';
const PreviousButton = () => { const PreviousButton = () => {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation('common');
const onClick = () => { const onClick = () => {
const { idp, step, token } = router.query as { idp: string; step: string; token: string }; const { idp, step, token } = router.query as { idp: string; step: string; token: string };
@ -20,7 +21,7 @@ const PreviousButton = () => {
return ( return (
<div> <div>
<ButtonOutline onClick={onClick}>Previous Step</ButtonOutline> <ButtonOutline onClick={onClick}>{t('previous_step')}</ButtonOutline>
</div> </div>
); );
}; };

View File

@ -196,7 +196,7 @@ const CreateSetupLink = ({ service }: { service: SetupLinkService }) => {
<textarea <textarea
id={'redirectUrl'} id={'redirectUrl'}
name='redirectUrl' name='redirectUrl'
placeholder={'Allowed redirect URLs (newline separated)'} placeholder={t('allowed_redirect_url')}
value={formObj['redirectUrl']} value={formObj['redirectUrl']}
required required
onChange={handleChange} onChange={handleChange}

View File

@ -1,11 +1,12 @@
import { useTranslation } from 'next-i18next';
const InvalidSetupLinkAlert = ({ message }: { message: string }) => { const InvalidSetupLinkAlert = ({ message }: { message: string }) => {
const { t } = useTranslation('common');
return ( return (
<div className='flex flex-col gap-3 rounded border border-error p-4'> <div className='flex flex-col gap-3 rounded border border-error p-4'>
<h3 className='text-base font-medium'>{message}</h3> <h3 className='text-base font-medium'>{message}</h3>
<p className='leading-6'> <p className='leading-6'>{t('invalid_setup_link_alert')}</p>
Please contact your administrator to get a new setup link. If you are the administrator, visit the
Admin Portal to create a new setup link for the service.
</p>
</div> </div>
); );
}; };

View File

@ -23,13 +23,10 @@ export const SetupLinkInfo = ({ setupLink, visible, onClose }: SetupLinkInfoProp
title={`Setup link info: tenant '${setupLink.tenant}', product '${setupLink.product}'`}> title={`Setup link info: tenant '${setupLink.tenant}', product '${setupLink.product}'`}>
<div className='mt-2 flex flex-col gap-3'> <div className='mt-2 flex flex-col gap-3'>
<div> <div>
<InputWithCopyButton <InputWithCopyButton text={setupLink.url} label={t('share_setup_link')} />
text={setupLink.url}
label='Share this link with your customer to setup their service'
/>
</div> </div>
<p className='text-sm'> <p className='text-sm'>
This link is valid till{' '} {t('setup_link_valid_till')}{' '}
<span className={new Date(setupLink.validTill) < new Date() ? 'text-red-400' : ''}> <span className={new Date(setupLink.validTill) < new Date() ? 'text-red-400' : ''}>
{new Date(setupLink.validTill).toString()} {new Date(setupLink.validTill).toString()}
</span> </span>

View File

@ -113,11 +113,11 @@ function BlocklyComponent(props) {
/> />
</div> </div>
<div className='mb-6 w-full px-3 md:mb-0 md:w-1/3'> <div className='mb-6 w-full px-3 md:mb-0 md:w-1/3'>
<ButtonPrimary onClick={uploadModel}>Publish Model</ButtonPrimary> <ButtonPrimary onClick={uploadModel}>{t('publish_model')}</ButtonPrimary>
</div> </div>
<div className='mb-6 w-full px-3 md:mb-0 md:w-1/3'> <div className='mb-6 w-full px-3 md:mb-0 md:w-1/3'>
<ButtonBase color='secondary' onClick={toggleRetrieveConfirm}> <ButtonBase color='secondary' onClick={toggleRetrieveConfirm}>
Retrieve Model {t('retrieve_model')}
</ButtonBase> </ButtonBase>
</div> </div>
</div> </div>

View File

@ -162,10 +162,7 @@ const UpdateApp = ({ hasValidLicense }: { hasValidLicense: boolean }) => {
/> />
</div> </div>
<div className='pt-4'> <div className='pt-4'>
<p className='text-base leading-6 text-gray-500'> <p className='text-base leading-6 text-gray-500'>{t('customize_branding')}:</p>
You can customize the look and feel Identity Provider selection page by setting following
options:
</p>
</div> </div>
<div className='form-control w-full md:w-1/2'> <div className='form-control w-full md:w-1/2'>
<label className='label'> <label className='label'>

View File

@ -171,6 +171,8 @@
"assertion_encryption": "Assertion Encryption", "assertion_encryption": "Assertion Encryption",
"sp_saml_config_title": "Service Provider (SP) SAML Configuration", "sp_saml_config_title": "Service Provider (SP) SAML Configuration",
"sp_saml_config_description": "Your Identity Provider (IdP) will ask for the following information while configuring the SAML application. Share this information with your IT administrator.", "sp_saml_config_description": "Your Identity Provider (IdP) will ask for the following information while configuring the SAML application. Share this information with your IT administrator.",
"refer_to_provider_instructions": "Refer to our <guideLink>guides</guideLink> for provider specific instructions.",
"sp_download_our_public_cert": "If you want to encrypt the assertion, you can <downloadLink>download our public certificate.</downloadLink> Otherwise select the Unencrypted option.",
"sp_oidc_config_title": "Service Provider (SP) OIDC Configuration", "sp_oidc_config_title": "Service Provider (SP) OIDC Configuration",
"sp_oidc_config_description": "Your Identity Provider (IdP) will ask for the following information while configuring the OIDC application. Share this information with your IT administrator.", "sp_oidc_config_description": "Your Identity Provider (IdP) will ask for the following information while configuring the OIDC application. Share this information with your IT administrator.",
"password": "Password", "password": "Password",
@ -214,5 +216,52 @@
"directory_domain": "Directory Domain", "directory_domain": "Directory Domain",
"dsync_google_auth_url": "The URL that you will need to authorize the application to access your Google Directory.", "dsync_google_auth_url": "The URL that you will need to authorize the application to access your Google Directory.",
"show_secret": "Show secret", "show_secret": "Show secret",
"hide_secret": "Hide secret" "hide_secret": "Hide secret",
"directory": "Directory",
"users": "Users",
"groups": "Groups",
"webhook_events": "Webhook Events",
"sp_metadata": "SP Metadata",
"saml_configuration": "SAML Configuration",
"saml_public_cert": "SAML Public Certificate",
"oidc_configuration": "OpenID Configuration",
"oidc_discovery": "OpenID Connect Discovery",
"idp_metadata": "IdP Metadata",
"idp_configuration": "IdP Configuration",
"idp_configuration_title": "Identity Provider Configuration",
"idp_configuration_description": "Links for SAML/OIDC IdP setup",
"idp_configuration_label": "Identity Provider Configuration links",
"auth_integration_title": "Auth integration",
"auth_integration_description": "Links for OAuth 2.0/OpenID Connect auth",
"auth_integration_label": "Auth integration links",
"saml_fed_configuration_title": "SAML Federation",
"saml_fed_configuration_description": "Links for SAML Federation app setup",
"saml_fed_configuration_label": "SAML Federation links",
"retraced_project_created": "Project created successfully",
"project_name": "Project name",
"create_project": "Create Project",
"share_setup_link": "Share this link with your customer to setup their service",
"setup_link_valid_till": "This link is valid till",
"invalid_setup_link_alert": "Please contact your administrator to get a new setup link. If you are the administrator, visit the Admin Portal to create a new setup link for the service.",
"boxyhq_admin_portal": "BoxyHQ Admin Portal",
"environment:": "Environment",
"group_or_tenant": "Group (Tenant)",
"id": "Id",
"created_at": "Created At",
"previous_step": "Previous Step",
"next_step": "Next Step",
"boxyhq_powered_by": "Powered by BoxyHQ",
"publish_model": "Publish Model",
"retrieve_model": "Retrieve Model",
"guides": "guides",
"learn_to_enable_auth_methods": "Please visit <docLink>BoxyHQ documentation</docLink> to learn how to enable the Magic Link or Email/Password authentication methods.",
"advanced_sp_saml_configuration": "Advanced Service Provider (SP) SAML Configuration",
"select_identity_provider": "Select Identity Provider",
"configure_identity_provider": "Configure {{provider}}",
"change_identity_provider": "Change Identity Provider",
"invalid_request_try_again": "Invalid request. Please try again.",
"choose_an_identity_provider_to_continue": "Choose an Identity Provider to continue. If you don't see your Identity Provider, please contact your administrator.",
"choose_an_app_to_continue": "Choose an app to continue. If you don't see your app, please contact your administrator.",
"no_saml_response_try_again": "No SAMLResponse found. Please try again.",
"customize_branding": "You can customize the look and feel Identity Provider selection page by setting following options"
} }

23
package-lock.json generated
View File

@ -56,6 +56,7 @@
"eslint": "8.56.0", "eslint": "8.56.0",
"eslint-config-next": "14.0.4", "eslint-config-next": "14.0.4",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "9.1.0",
"eslint-plugin-i18next": "6.0.3",
"postcss": "8.4.32", "postcss": "8.4.32",
"prettier": "3.1.1", "prettier": "3.1.1",
"prettier-plugin-tailwindcss": "0.5.9", "prettier-plugin-tailwindcss": "0.5.9",
@ -7748,6 +7749,19 @@
"ms": "^2.1.1" "ms": "^2.1.1"
} }
}, },
"node_modules/eslint-plugin-i18next": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-i18next/-/eslint-plugin-i18next-6.0.3.tgz",
"integrity": "sha512-RtQXYfg6PZCjejIQ/YG+dUj/x15jPhufJ9hUDGH0kCpJ6CkVMAWOQ9exU1CrbPmzeykxLjrXkjAaOZF/V7+DOA==",
"dev": true,
"dependencies": {
"lodash": "^4.17.21",
"requireindex": "~1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/eslint-plugin-import": { "node_modules/eslint-plugin-import": {
"version": "2.29.0", "version": "2.29.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz",
@ -16871,6 +16885,15 @@
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"dev": true "dev": true
}, },
"node_modules/requireindex": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz",
"integrity": "sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==",
"dev": true,
"engines": {
"node": ">=0.10.5"
}
},
"node_modules/requires-port": { "node_modules/requires-port": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",

View File

@ -100,6 +100,7 @@
"eslint": "8.56.0", "eslint": "8.56.0",
"eslint-config-next": "14.0.4", "eslint-config-next": "14.0.4",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "9.1.0",
"eslint-plugin-i18next": "6.0.3",
"postcss": "8.4.32", "postcss": "8.4.32",
"prettier": "3.1.1", "prettier": "3.1.1",
"prettier-plugin-tailwindcss": "0.5.9", "prettier-plugin-tailwindcss": "0.5.9",

View File

@ -4,7 +4,7 @@ import { useRouter } from 'next/router';
import Link from 'next/link'; import Link from 'next/link';
import Image from 'next/image'; import Image from 'next/image';
import { useSession, getCsrfToken, signIn, SessionProvider } from 'next-auth/react'; import { useSession, getCsrfToken, signIn, SessionProvider } from 'next-auth/react';
import { useTranslation } from 'next-i18next'; import { useTranslation, Trans } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { errorToast, successToast } from '@components/Toaster'; import { errorToast, successToast } from '@components/Toaster';
import { ButtonOutline } from '@components/ButtonOutline'; import { ButtonOutline } from '@components/ButtonOutline';
@ -114,7 +114,9 @@ const Login = ({
<div className='flex justify-center'> <div className='flex justify-center'>
<Image src='/logo.png' alt='BoxyHQ logo' width={50} height={50} /> <Image src='/logo.png' alt='BoxyHQ logo' width={50} height={50} />
</div> </div>
<h2 className='text-center text-3xl font-extrabold text-gray-900'>BoxyHQ Admin Portal</h2> <h2 className='text-center text-3xl font-extrabold text-gray-900'>
{t('boxyhq_admin_portal')}
</h2>
<p className='text-center text-sm text-gray-600'>{t('boxyhq_tagline')}</p> <p className='text-center text-sm text-gray-600'>{t('boxyhq_tagline')}</p>
</div> </div>
@ -170,17 +172,21 @@ const Login = ({
{/* No login methods enabled */} {/* No login methods enabled */}
{!isEmailPasswordEnabled && !isMagicLinkEnabled && ( {!isEmailPasswordEnabled && !isMagicLinkEnabled && (
<div className='mt-10 text-center font-medium text-gray-600'> <div className='mt-10 text-center font-medium text-gray-600'>
<p> <Trans
Please visit&nbsp; i18nKey='learn_to_enable_auth_methods'
<a t={t}
href='https://boxyhq.com/docs/admin-portal/overview' components={{
target='_blank' docLink: (
rel='noopener noreferrer' <a
className='underline underline-offset-2'> href='https://boxyhq.com/docs/admin-portal/overview'
BoxyHQ documentation target='_blank'
</a> rel='noopener noreferrer'
&nbsp;to learn how to enable the Magic Link or Email/Password authentication methods. className='underline underline-offset-2'>
</p> {t('documentation')}
</a>
),
}}
/>
</div> </div>
)} )}

View File

@ -9,6 +9,7 @@ import ErrorMessage from '@components/Error';
import { LinkBack } from '@components/LinkBack'; import { LinkBack } from '@components/LinkBack';
import { Select } from 'react-daisyui'; import { Select } from 'react-daisyui';
import { retracedOptions } from '@lib/env'; import { retracedOptions } from '@lib/env';
import { useTranslation } from 'next-i18next';
const LogsViewer = dynamic(() => import('@components/retraced/LogsViewer'), { const LogsViewer = dynamic(() => import('@components/retraced/LogsViewer'), {
ssr: false, ssr: false,
@ -20,6 +21,7 @@ export interface Props {
const Events: NextPage<Props> = ({ host }: Props) => { const Events: NextPage<Props> = ({ host }: Props) => {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation('common');
const [environment, setEnvironment] = useState(''); const [environment, setEnvironment] = useState('');
const [group, setGroup] = useState(''); const [group, setGroup] = useState('');
@ -62,7 +64,7 @@ const Events: NextPage<Props> = ({ host }: Props) => {
<div className='flex space-x-2'> <div className='flex space-x-2'>
<div className='form-control max-w-xs'> <div className='form-control max-w-xs'>
<label className='label pl-0'> <label className='label pl-0'>
<span className='label-text'>Environment</span> <span className='label-text'>{t('environment')}</span>
</label> </label>
{project ? ( {project ? (
<Select <Select
@ -81,7 +83,7 @@ const Events: NextPage<Props> = ({ host }: Props) => {
</div> </div>
<div className='form-control max-w-xs'> <div className='form-control max-w-xs'>
<label className='label pl-0'> <label className='label pl-0'>
<span className='label-text'>Group (Tenant)</span> <span className='label-text'>{t('group_or_tenant')}</span>
</label> </label>
{groups ? ( {groups ? (
<Select <Select

View File

@ -32,7 +32,7 @@ const ProjectList: NextPage = () => {
return ( return (
<div> <div>
<div className='mb-5 flex items-center justify-between'> <div className='mb-5 flex items-center justify-between'>
<h2 className='font-bold text-gray-700 dark:text-white md:text-xl'>Projects</h2> <h2 className='font-bold text-gray-700 dark:text-white md:text-xl'>{t('projects')}</h2>
<LinkPrimary Icon={PlusIcon} href={'/admin/retraced/projects/new'}> <LinkPrimary Icon={PlusIcon} href={'/admin/retraced/projects/new'}>
{t('new_project')} {t('new_project')}
</LinkPrimary> </LinkPrimary>
@ -46,16 +46,16 @@ const ProjectList: NextPage = () => {
<thead className='bg-gray-50 text-xs uppercase text-gray-700 dark:bg-gray-700 dark:text-gray-400'> <thead className='bg-gray-50 text-xs uppercase text-gray-700 dark:bg-gray-700 dark:text-gray-400'>
<tr> <tr>
<th scope='col' className='px-6 py-3'> <th scope='col' className='px-6 py-3'>
Name {t('name')}
</th> </th>
<th scope='col' className='px-6 py-3'> <th scope='col' className='px-6 py-3'>
Id {t('id')}
</th> </th>
<th scope='col' className='px-6 py-3'> <th scope='col' className='px-6 py-3'>
Created At {t('created_at')}
</th> </th>
<th scope='col' className='px-6 py-3'> <th scope='col' className='px-6 py-3'>
Actions {t('actions')}
</th> </th>
</tr> </tr>
</thead> </thead>

View File

@ -58,7 +58,7 @@ const SAMLTraceInspector: NextPage = () => {
<h3 className='text-base font-semibold leading-6 text-gray-900'>{t('trace_details')}</h3> <h3 className='text-base font-semibold leading-6 text-gray-900'>{t('trace_details')}</h3>
<p className='mt-1 flex max-w-2xl gap-6 text-sm text-gray-500'> <p className='mt-1 flex max-w-2xl gap-6 text-sm text-gray-500'>
<span className='whitespace-nowrap'> <span className='whitespace-nowrap'>
<span className='font-medium text-gray-500'>TraceID:</span> <span className='font-medium text-gray-500'>{t('trace_id')}</span>
<span className='ml-2 font-bold text-gray-700'> {traceId}</span> <span className='ml-2 font-bold text-gray-700'> {traceId}</span>
</span> </span>
<span className='whitespace-nowrap'> <span className='whitespace-nowrap'>

View File

@ -53,7 +53,7 @@ export default function ChooseIdPConnection({
{authFlow in selectors ? ( {authFlow in selectors ? (
selectors[authFlow] selectors[authFlow]
) : ( ) : (
<p className='text-center text-sm text-slate-600'>Invalid request. Please try again.</p> <p className='text-center text-sm text-slate-600'>{t('invalid_request_try_again')}</p>
)} )}
</div> </div>
<div className='my-4'> <div className='my-4'>
@ -98,10 +98,7 @@ const IdpSelector = ({ connections }: { connections: Connection[] }) => {
); );
})} })}
</ul> </ul>
<p className='text-center text-sm text-slate-600'> <p className='text-center text-sm text-slate-600'>{t('choose_an_identity_provider_to_continue')}</p>
Choose an Identity Provider to continue. If you don&apos;t see your Identity Provider, please contact
your administrator.
</p>
</> </>
); );
}; };
@ -143,7 +140,7 @@ const AppSelector = ({
}; };
if (!SAMLResponse) { if (!SAMLResponse) {
return <p className='text-center text-sm text-slate-600'>No SAMLResponse found. Please try again.</p>; return <p className='text-center text-sm text-slate-600'>{t('no_saml_response_try_again')}</p>;
} }
return ( return (
@ -171,9 +168,7 @@ const AppSelector = ({
})} })}
</ul> </ul>
</form> </form>
<p className='text-center text-sm text-slate-600'> <p className='text-center text-sm text-slate-600'>{t('choose_an_app_to_continue')}</p>
Choose an app to continue. If you don&apos;t see your app, please contact your administrator.
</p>
</> </>
); );
}; };

View File

@ -3,6 +3,7 @@ import type { NextPage } from 'next';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import Loading from '@components/Loading'; import Loading from '@components/Loading';
import useSetupLink from '@lib/ui/hooks/useSetupLink'; import useSetupLink from '@lib/ui/hooks/useSetupLink';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
const SetupLinkIndexPage: NextPage = () => { const SetupLinkIndexPage: NextPage = () => {
const router = useRouter(); const router = useRouter();
@ -29,4 +30,14 @@ const SetupLinkIndexPage: NextPage = () => {
return null; return null;
}; };
export const getServerSideProps = async (context) => {
const { locale } = context;
return {
props: {
...(await serverSideTranslations(locale, ['common'])),
},
};
};
export default SetupLinkIndexPage; export default SetupLinkIndexPage;

View File

@ -10,6 +10,7 @@ import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'nex
import type { MDXRemoteSerializeResult } from 'next-mdx-remote'; import type { MDXRemoteSerializeResult } from 'next-mdx-remote';
import { ArrowsRightLeftIcon } from '@heroicons/react/24/outline'; import { ArrowsRightLeftIcon } from '@heroicons/react/24/outline';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useTranslation } from 'next-i18next';
import jackson from '@lib/jackson'; import jackson from '@lib/jackson';
import { jacksonOptions } from '@lib/env'; import { jacksonOptions } from '@lib/env';
@ -24,10 +25,12 @@ import SelectIdentityProviders from '@components/setup-link-instructions/SelectI
type NewConnectionProps = InferGetServerSidePropsType<typeof getServerSideProps>; type NewConnectionProps = InferGetServerSidePropsType<typeof getServerSideProps>;
const AdvancedSPConfigLink = () => { const AdvancedSPConfigLink = () => {
const { t } = useTranslation('common');
return ( return (
<div className='py-2'> <div className='py-2'>
<Link href='/.well-known/saml-configuration' target='_blank' className='underline-offset-4'> <Link href='/.well-known/saml-configuration' target='_blank' className='underline-offset-4'>
<span className='text-xs'>Advanced Service Provider (SP) SAML Configuration</span> <span className='text-xs'>{t('advanced_sp_saml_configuration')}</span>
</Link> </Link>
</div> </div>
); );
@ -72,6 +75,8 @@ const NewConnection = ({
publicCertUrl, publicCertUrl,
oidcCallbackUrl, oidcCallbackUrl,
}: NewConnectionProps) => { }: NewConnectionProps) => {
const { t } = useTranslation('common');
const linkSelectIdp = { pathname: '/setup/[token]/sso-connection/new', query: { token: setupLinkToken } }; const linkSelectIdp = { pathname: '/setup/[token]/sso-connection/new', query: { token: setupLinkToken } };
const scope = { const scope = {
@ -108,9 +113,9 @@ const NewConnection = ({
} }
progress = (100 / selectedIdP.stepCount) * parseInt(step); progress = (100 / selectedIdP.stepCount) * parseInt(step);
heading = `Configure ${selectedIdP?.name}`; heading = t('configure_identity_provider', { provider: selectedIdP.name });
} else { } else {
heading = 'Select Identity Provider'; heading = t('select_identity_provider');
} }
return ( return (
@ -121,7 +126,7 @@ const NewConnection = ({
{source && ( {source && (
<Link className='btn btn-xs h-0' href={linkSelectIdp}> <Link className='btn btn-xs h-0' href={linkSelectIdp}>
<ArrowsRightLeftIcon className='w-5 h-5' /> <ArrowsRightLeftIcon className='w-5 h-5' />
Change Identity Provider {t('change_identity_provider')}
</Link> </Link>
)} )}
</div> </div>

View File

@ -1,6 +1,6 @@
import type { NextPage, InferGetStaticPropsType } from 'next'; import type { NextPage, InferGetStaticPropsType } from 'next';
import React from 'react'; import React from 'react';
import { useTranslation } from 'next-i18next'; import { useTranslation, Trans } from 'next-i18next';
import jackson from '@lib/jackson'; import jackson from '@lib/jackson';
import { InputWithCopyButton } from '@components/ClipboardButton'; import { InputWithCopyButton } from '@components/ClipboardButton';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
@ -18,15 +18,21 @@ const SPConfig: NextPage<InferGetStaticPropsType<typeof getServerSideProps>> = (
<h2 className='font-bold text-gray-700 md:text-xl'>{t('sp_oidc_config_title')}</h2> <h2 className='font-bold text-gray-700 md:text-xl'>{t('sp_oidc_config_title')}</h2>
<p className='text-sm leading-6 text-gray-800'>{t('sp_oidc_config_description')}</p> <p className='text-sm leading-6 text-gray-800'>{t('sp_oidc_config_description')}</p>
<p className='text-sm leading-6 text-gray-600'> <p className='text-sm leading-6 text-gray-600'>
Refer to our&nbsp; <Trans
<a i18nKey='refer_to_provider_instructions'
href='https://boxyhq.com/docs/jackson/sso-providers' t={t}
target='_blank' components={{
rel='noreferrer' guideLink: (
className='underline underline-offset-4'> <a
guides href='https://boxyhq.com/docs/jackson/sso-providers'
</a> target='_blank'
&nbsp;for provider specific instructions. rel='noreferrer'
className='underline underline-offset-4'>
{t('guides')}
</a>
),
}}
/>
</p> </p>
</div> </div>
<div className='mt-6 flex flex-col gap-6'> <div className='mt-6 flex flex-col gap-6'>

View File

@ -1,7 +1,7 @@
import type { NextPage, InferGetStaticPropsType } from 'next'; import type { NextPage, InferGetStaticPropsType } from 'next';
import Link from 'next/link'; import Link from 'next/link';
import React from 'react'; import React from 'react';
import { useTranslation } from 'next-i18next'; import { useTranslation, Trans } from 'next-i18next';
import jackson from '@lib/jackson'; import jackson from '@lib/jackson';
import { InputWithCopyButton } from '@components/ClipboardButton'; import { InputWithCopyButton } from '@components/ClipboardButton';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
@ -19,15 +19,21 @@ const SPConfig: NextPage<InferGetStaticPropsType<typeof getServerSideProps>> = (
<h2 className='font-bold text-gray-700 md:text-xl'>{t('sp_saml_config_title')}</h2> <h2 className='font-bold text-gray-700 md:text-xl'>{t('sp_saml_config_title')}</h2>
<p className='text-sm leading-6 text-gray-800'>{t('sp_saml_config_description')}</p> <p className='text-sm leading-6 text-gray-800'>{t('sp_saml_config_description')}</p>
<p className='text-sm leading-6 text-gray-600'> <p className='text-sm leading-6 text-gray-600'>
Refer to our&nbsp; <Trans
<a i18nKey='refer_to_provider_instructions'
href='https://boxyhq.com/docs/jackson/sso-providers' t={t}
target='_blank' components={{
rel='noreferrer' guideLink: (
className='underline underline-offset-4'> <a
guides href='https://boxyhq.com/docs/jackson/sso-providers'
</a> target='_blank'
&nbsp;for provider specific instructions. rel='noreferrer'
className='underline underline-offset-4'>
{t('guides')}
</a>
),
}}
/>
</p> </p>
</div> </div>
<div className='mt-6 flex flex-col gap-6'> <div className='mt-6 flex flex-col gap-6'>
@ -67,11 +73,20 @@ const SPConfig: NextPage<InferGetStaticPropsType<typeof getServerSideProps>> = (
{t('assertion_encryption')} {t('assertion_encryption')}
</label> </label>
<p className='text-sm'> <p className='text-sm'>
If you want to encrypt the assertion, you can&nbsp; <Trans
<Link href='/.well-known/saml.cer' className='underline underline-offset-4' target='_blank'> i18nKey='sp_download_our_public_cert'
download our public certificate. t={t}
</Link> components={{
&nbsp;Otherwise select the Unencrypted option. downloadLink: (
<Link
href='/.well-known/saml.cer'
className='underline underline-offset-4'
target='_blank'>
{t('download')}
</Link>
),
}}
/>
</p> </p>
</div> </div>
</div> </div>