Merge branch 'main' into feat/save-data-on-blur

This commit is contained in:
Catalin Pit
2024-09-11 10:26:39 +03:00
committed by GitHub
81 changed files with 2273 additions and 1904 deletions

View File

@ -27,6 +27,8 @@ NEXT_PRIVATE_OIDC_SKIP_VERIFY=""
# [[URLS]] # [[URLS]]
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000" NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
NEXT_PUBLIC_MARKETING_URL="http://localhost:3001" NEXT_PUBLIC_MARKETING_URL="http://localhost:3001"
# URL used by the web app to request itself (e.g. local background jobs)
NEXT_PRIVATE_INTERNAL_WEBAPP_URL="http://localhost:3000"
# [[DATABASE]] # [[DATABASE]]
NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso" NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"

View File

@ -32,6 +32,9 @@ jobs:
- name: Run Playwright tests - name: Run Playwright tests
run: npm run ci run: npm run ci
env:
# Needed since we use next start which will set the NODE_ENV to production
NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH: './example/cert.p12'
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: always() if: always()

View File

@ -5,6 +5,8 @@ description: Learn how to self-host Documenso on your server or cloud infrastruc
import { Callout, Steps } from 'nextra/components'; import { Callout, Steps } from 'nextra/components';
import { CallToAction } from '@documenso/ui/components/call-to-action';
# Self Hosting # Self Hosting
We support various deployment methods and are actively working on adding more. Please let us know if you have a specific deployment method in mind! We support various deployment methods and are actively working on adding more. Please let us know if you have a specific deployment method in mind!
@ -273,3 +275,5 @@ We offer several alternative deployment methods for Documenso if you need more o
## Koyeb ## Koyeb
[![Deploy to Koyeb](https://www.koyeb.com/static/images/deploy/button.svg)](https://app.koyeb.com/deploy?type=git&repository=github.com/documenso/documenso&branch=main&name=documenso-app&builder=dockerfile&dockerfile=/docker/Dockerfile) [![Deploy to Koyeb](https://www.koyeb.com/static/images/deploy/button.svg)](https://app.koyeb.com/deploy?type=git&repository=github.com/documenso/documenso&branch=main&name=documenso-app&builder=dockerfile&dockerfile=/docker/Dockerfile)
<CallToAction className="mt-12" utmSource="self-hosting" />

View File

@ -3,6 +3,10 @@ title: Getting Started with Self-Hosting
description: A step-by-step guide to setting up and hosting your own Documenso instance. description: A step-by-step guide to setting up and hosting your own Documenso instance.
--- ---
import { CallToAction } from '@documenso/ui/components/call-to-action';
# Getting Started with Self-Hosting # Getting Started with Self-Hosting
This is a step-by-step guide to setting up and hosting your own Documenso instance. Before getting started, [select the right license for you](/users/licenses). This is a step-by-step guide to setting up and hosting your own Documenso instance. Before getting started, [select the right license for you](/users/licenses).
<CallToAction className="mt-12" utmSource="self-hosting" />

View File

@ -0,0 +1,72 @@
---
title: 'Introducing Embedding Support for Documenso'
description: 'Embedding is now here! Learn how we built it and how it can be used to bring e-signing to your own applications.'
authorName: 'Lucas Smith'
authorImage: '/blog/blog-author-lucas.png'
authorRole: 'Co-Founder'
date: 2024-09-06
tags:
- Development
---
When we first launched Documenso, one of the most requested features was embedding. We knew it was important and aligned with our desire to not just be a e-signing application but to instead provide the e-signature infrastructure for the web and beyond.
With that said, we decided to hold off initially so we could focus on building a solid, well-featured core application. Looking back, this was definitely the right call. Embedding is only as good as the features behind it, and we didn't want to release something that wasn't ready to meet user and developer expectations.
Over the past year, we've been busy adding tons of new features and reaching new levels of compliance, like 21 CFR Part 11. We've also introduced [new fields](/blog/introducing-advanced-signing-fields), [built out an API](/blog/public-api), [added webhooks, integrations with Zapier](/blog/launch-week-2-day-4), and a lot more.
Now that we've laid a solid foundation, it's finally time to focus on embedding, the top-requested feature from both our users and those self-hosting our platform.
## Why Embedding Took Time
In previous projects, Ive often seen embedding built by bundling components for use in a clients website or app. This method gives users maximum flexibility for styling and behavior, while avoiding certain cross-origin issues. However, it can also introduce problems like code conflicts or performance bottlenecks. For example, third-party tools such as Google Tag Manager (GTM) or other marketing scripts can interfere with your SDK. Additionally, the SDK must remain lightweight to avoid slowing down the clients page.
For Documenso, we decided to explore a different approach. After carefully researching our options, we opted for an iframe-based solution. While iframes are typically less flexible—especially when it comes to theming or passing pre-filled data containing personally identifiable information (PII)—we identified ways to mitigate these concerns.
One of the biggest challenges was ensuring that we could pass sensitive data, like emails for pre-filling forms, without exposing PII to our server. To solve this, we used [fragment identifiers](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) in the URL, which are processed client-side and never sent in network requests. This method ensures that PII is protected and not logged by our server or any intermediate web services.
### Using the PostMessage API for Communication
To maintain a high level of interactivity, our iframes communicate with the parent window using the [postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage). This allows us to notify the parent app when specific events occur inside the iframe, creating a more dynamic user experience and bridging the gap between our iframe-based solution and typical fat SDKs.
Additionally, props are passed into the iframe via the [fragment identifier](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) of the URL. This avoids the need for complex two-way data synchronization between the parent and child frames, making the system stable and more reliable.
### Building the Embeds with Mitosis
Given that our iframe solution is quite lightweight, we saw this as a great opportunity to experiment with [Mitosis](https://mitosis.builder.io/) which would let us do something truly special. For those unfamiliar, Mitosis is a project by Builder.io that lets you write components once and then transpile them into a variety of frameworks like React, Vue, and Svelte.
We used Mitosis to build two key components: a direct template embed and a document signing embed. The direct template allows users to use a template as if it were an evergreen document—meaning that, when someone completes the template, a new document is automatically generated. This is the use case we expect most users to adopt for embedding. For more advanced workflows, we also offer a document signing embed, which can handle multi-recipient workflows and other complex scenarios intended for use in deeper, rich integrations.
Mitosis allowed us to quickly target several popular frameworks, including [React](https://www.npmjs.com/package/@documenso/embed-react), [Preact](https://www.npmjs.com/package/@documenso/embed-preact), [Vue](https://www.npmjs.com/package/@documenso/embed-vue), [Svelte](https://www.npmjs.com/package/@documenso/embed-svelte), and [SolidJS](https://www.npmjs.com/package/@documenso/embed-solid).
I had also hoped to include Angular, but while Mitosis makes it really easy to transpile component, we still have to take care of bundling and packaging the resulting component ourselves. While the above frameworks can all be bundled using Vite.js, Angular still has it's own set of tooling that we would need to learn and use. Given this constraint we opted to put Angular on hold for now while we wait for the newer Vite.js support to mature.
### Challenges and Lessons with Mitosis and more
While our experience with Mitosis was largely positive, there were some challenges along the way. For instance, certain state properties with the same names as props caused issues during the transpilation process, leading to type errors and unexpected transpilation results with some targets.
This was also a challenge since our initial implementation of the two components had some minor separation of concerns which also resulted in some transpilation issues with some targets. We addressed this by removing the separation of concerns for now since it was mostly for show rather than out of necessity.
On top of that, packaging and publishing the embeds posed its own set of challenges, particularly given the growing complexity of JavaScript package management. Tools like [Publint](https://www.npmjs.com/package/publint) helped streamline the process by ensuring we followed best practices for both CommonJS and ESM formats.
### To the Future, The Documenso Platform
With the embedding feature now in place, we're excited to continue expanding Documenso's capabilities. Embeds are just the beginning of what we're calling the Documenso platform. Through our user research, we've learned that while many businesses appreciate having a flexible e-signature solution, they're even more interested in using our tools to build signing functionality directly into their own apps—without worrying about the technical complexities of compliance and security that come with e-signing.
Over the coming months, we'll be working on enhancing our API, strengthening integrations with tools like Zapier, and improving our webhook system. Our goal is to give users the ability to embed e-signatures and document management wherever they need it, whether that's through self-hosting or by using Documenso as a platform. We can't wait to see how our users and self-hosters leverage these new capabilities!
### Ready to Get Started?
If you're ready to embed document signing into your own app or website, check out our [Embedding Documentation](https://docs.documenso.com/developers/embedding?utm_source=blog&utm_campaign=introducing-embedding) to see how easy it is to get started. You'll find everything you need to get started today!
<video
src="/blog/introducing-embedding/embedding-demo.mp4"
className="aspect-video w-full"
autoPlay
loop
controls
/>
We're always here to help! If you have questions or need support, join our [Discord](https://documen.so/discord) or [book a demo](https://documen.so/book-a-demo). We'd love to hear how you're using Documenso or wanting to use Documenso to enhance your workflow.
Stay tuned for more updates as we continue to evolve the Documenso platform and make it even easier to bring document signing into your workflows.

View File

@ -8,6 +8,80 @@ Check out what's new in the latest version and read our thoughts on it. For more
--- ---
# Documenso v1.7.0: Embedded Signing, Copy and Paste, and More
We're thrilled to announce the release of Documenso v1.7.0, packed with exciting new features and improvements that enhance document signing flexibility, user experience, and global accessibility.
We're excited to see what you'll create with this release and we'd love to hear your feedback. Let's dive into the highlights:
## 🌟 Key Features
### Embedded Signing Experience
Take your document signing to the next level with our new embedded signing feature. Now you can seamlessly integrate Documenso's signing process directly into your own website or application, providing a smooth, branded experience for your users.
<video
src="/blog/introducing-embedding/embedding-demo.mp4"
className="aspect-video w-full"
controls
/>
Check out our [Embedding documentation](https://docs.documenso.com/developers/embedding) to learn more about how to get started.
### Copy and Paste Fields
Streamline your document preparation with our new copy and paste functionality for fields. This feature allows you to quickly duplicate fields across your document, saving time and ensuring consistency in your templates.
### Customizable Signature Colors
Recipients can now select a signature color from our list of available colors, supporting workflows where specific colors are required for each recipient, location, or document.
### Enhanced Internationalization (i18n)
Following on from our last release we've now expanded our i18n support to the main web application. We haven't yet added support for any additional languages but that will be coming quickly now that we have completed the hard work of wrapping all of our content in our new i18n system.
These enhancements make Documenso more accessible to users worldwide.
## 🔧 Other Improvements
- **API Enhancements**:
- New endpoint to prefill fields via API
- Updated createFields API endpoint for more flexibility
- Automatically set public profile URL for OIDC users
- **Security and Performance**:
- Document sealing moved to a background job for improved performance
- Disable 2FA with backup codes for enhanced account recovery options
- Extended lifespan for invites and confirmations
- **User Experience**:
- Updated email templates to reflect team-specific information
- Fixed issues with dialog closing on page refresh
- Improved field editing in document templates
- **Other Items**:
- Added Elestio as a one-click deploy option
- Updated README for manual self-hosting
- New environment variable for internal webapp URL configuration
## 📚 New Content
- [Advanced fields article to help you make the most of Documenso's capabilities](/blog/introducing-advanced-signing-fields)
- [Embedding blog post to guide you through how we implemented embedding](/blog/introducing-embedding)
## 👏 Community Contributions
A big thank you to our vibrant community! This release includes contributions from several new contributors, further enriching Documenso's capabilities.
We're excited to see how you'll use these new features to streamline your document workflows. As always, we appreciate your feedback and support in making Documenso the best open-source document signing solution available.
Enjoy exploring v1.7.0!
---
# Documenso v1.6.1: Internationalization, Enhanced OIDC, and More # Documenso v1.6.1: Internationalization, Enhanced OIDC, and More
We're excited to announce the release of Documenso v1.6.1, which brings several improvements to enhance your document signing experience. Here are the key updates: We're excited to announce the release of Documenso v1.6.1, which brings several improvements to enhance your document signing experience. Here are the key updates:

View File

@ -1,6 +1,6 @@
{ {
"name": "@documenso/marketing", "name": "@documenso/marketing",
"version": "1.7.0-rc.4", "version": "1.7.1-rc.0",
"private": true, "private": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {

View File

@ -2,7 +2,8 @@ declare namespace NodeJS {
export interface ProcessEnv { export interface ProcessEnv {
NEXT_PUBLIC_WEBAPP_URL?: string; NEXT_PUBLIC_WEBAPP_URL?: string;
NEXT_PUBLIC_MARKETING_URL?: string; NEXT_PUBLIC_MARKETING_URL?: string;
NEXT_PRIVATE_INTERNAL_WEBAPP_URL?:string;
NEXT_PRIVATE_DATABASE_URL: string; NEXT_PRIVATE_DATABASE_URL: string;
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string; NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;

View File

@ -1,7 +1,6 @@
import { Suspense } from 'react'; import { Suspense } from 'react';
import { Caveat, Inter } from 'next/font/google'; import { Caveat, Inter } from 'next/font/google';
import { cookies, headers } from 'next/headers';
import { AxiomWebVitals } from 'next-axiom'; import { AxiomWebVitals } from 'next-axiom';
import { PublicEnvScript } from 'next-runtime-env'; import { PublicEnvScript } from 'next-runtime-env';
@ -10,8 +9,6 @@ import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/featur
import { I18nClientProvider } from '@documenso/lib/client-only/providers/i18n.client'; import { I18nClientProvider } from '@documenso/lib/client-only/providers/i18n.client';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server'; import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { NEXT_PUBLIC_MARKETING_URL } from '@documenso/lib/constants/app'; import { NEXT_PUBLIC_MARKETING_URL } from '@documenso/lib/constants/app';
import type { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n';
import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag'; import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag';
import { TrpcProvider } from '@documenso/trpc/react'; import { TrpcProvider } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
@ -59,25 +56,7 @@ export function generateMetadata() {
export default async function RootLayout({ children }: { children: React.ReactNode }) { export default async function RootLayout({ children }: { children: React.ReactNode }) {
const flags = await getAllAnonymousFlags(); const flags = await getAllAnonymousFlags();
let overrideLang: (typeof SUPPORTED_LANGUAGE_CODES)[number] | undefined; const { lang, locales, i18n } = setupI18nSSR();
// Should be safe to remove when we upgrade NextJS.
// https://github.com/vercel/next.js/pull/65008
// Currently if the middleware sets the cookie, it's not accessible in the cookies
// during the same render.
// So we go the roundabout way of checking the header for the set-cookie value.
if (!cookies().get('i18n')) {
const setCookieValue = headers().get('set-cookie');
const i18nCookie = setCookieValue?.split(';').find((cookie) => cookie.startsWith('i18n='));
if (i18nCookie) {
const i18n = i18nCookie.split('=')[1];
overrideLang = ZSupportedLanguageCodeSchema.parse(i18n);
}
}
const { lang, i18n } = setupI18nSSR(overrideLang);
return ( return (
<html <html
@ -105,7 +84,10 @@ export default async function RootLayout({ children }: { children: React.ReactNo
<ThemeProvider attribute="class" defaultTheme="system" enableSystem> <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<PlausibleProvider> <PlausibleProvider>
<TrpcProvider> <TrpcProvider>
<I18nClientProvider initialLocale={lang} initialMessages={i18n.messages}> <I18nClientProvider
initialLocaleData={{ lang, locales }}
initialMessages={i18n.messages}
>
{children} {children}
</I18nClientProvider> </I18nClientProvider>
</TrpcProvider> </TrpcProvider>

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import type { HTMLAttributes } from 'react'; import { type HTMLAttributes, useState } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
@ -9,15 +9,15 @@ import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { FaXTwitter } from 'react-icons/fa6'; import { FaXTwitter } from 'react-icons/fa6';
import { LiaDiscord } from 'react-icons/lia'; import { LiaDiscord } from 'react-icons/lia';
import { LuGithub } from 'react-icons/lu'; import { LuGithub, LuLanguages } from 'react-icons/lu';
import LogoImage from '@documenso/assets/logo.png'; import LogoImage from '@documenso/assets/logo.png';
import { cn } from '@documenso/ui/lib/utils'; import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
import { I18nSwitcher } from '~/components/(marketing)/i18n-switcher';
// import { StatusWidgetContainer } from './status-widget-container'; // import { StatusWidgetContainer } from './status-widget-container';
import { LanguageSwitcherDialog } from '@documenso/ui/components/common/language-switcher-dialog';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
export type FooterProps = HTMLAttributes<HTMLDivElement>; export type FooterProps = HTMLAttributes<HTMLDivElement>;
@ -44,7 +44,9 @@ const FOOTER_LINKS = [
]; ];
export const Footer = ({ className, ...props }: FooterProps) => { export const Footer = ({ className, ...props }: FooterProps) => {
const { _ } = useLingui(); const { _, i18n } = useLingui();
const [languageSwitcherOpen, setLanguageSwitcherOpen] = useState(false);
return ( return (
<div className={cn('border-t py-12', className)} {...props}> <div className={cn('border-t py-12', className)} {...props}>
@ -97,13 +99,22 @@ export const Footer = ({ className, ...props }: FooterProps) => {
</p> </p>
<div className="flex flex-row-reverse items-center sm:flex-row"> <div className="flex flex-row-reverse items-center sm:flex-row">
<I18nSwitcher className="text-muted-foreground ml-2 rounded-full font-normal sm:mr-2" /> <Button
className="text-muted-foreground ml-2 rounded-full font-normal sm:mr-2"
variant="ghost"
onClick={() => setLanguageSwitcherOpen(true)}
>
<LuLanguages className="mr-1.5 h-4 w-4" />
{SUPPORTED_LANGUAGES[i18n.locale]?.full || i18n.locale}
</Button>
<div className="flex flex-wrap"> <div className="flex flex-wrap">
<ThemeSwitcher /> <ThemeSwitcher />
</div> </div>
</div> </div>
</div> </div>
<LanguageSwitcherDialog open={languageSwitcherOpen} setOpen={setLanguageSwitcherOpen} />
</div> </div>
); );
}; };

View File

@ -1,71 +0,0 @@
import { useState } from 'react';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { CheckIcon } from 'lucide-react';
import { LuLanguages } from 'react-icons/lu';
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { switchI18NLanguage } from '@documenso/lib/server-only/i18n/switch-i18n-language';
import { dynamicActivate } from '@documenso/lib/utils/i18n';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import {
CommandDialog,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@documenso/ui/primitives/command';
type I18nSwitcherProps = {
className?: string;
};
export const I18nSwitcher = ({ className }: I18nSwitcherProps) => {
const { i18n, _ } = useLingui();
const [open, setOpen] = useState(false);
const [value, setValue] = useState(i18n.locale);
const setLanguage = async (lang: string) => {
setValue(lang);
setOpen(false);
await dynamicActivate(i18n, lang);
await switchI18NLanguage(lang);
};
return (
<>
<Button className={className} variant="ghost" onClick={() => setOpen(true)}>
<LuLanguages className="mr-1.5 h-4 w-4" />
{SUPPORTED_LANGUAGES[value]?.full || i18n.locale}
</Button>
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder={_(msg`Search languages...`)} />
<CommandList>
<CommandGroup>
{Object.values(SUPPORTED_LANGUAGES).map((language) => (
<CommandItem
key={language.short}
value={language.full}
onSelect={async () => setLanguage(language.short)}
>
<CheckIcon
className={cn(
'mr-2 h-4 w-4',
value === language.short ? 'opacity-100' : 'opacity-0',
)}
/>
{SUPPORTED_LANGUAGES[language.short].full}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</CommandDialog>
</>
);
};

View File

@ -1,39 +0,0 @@
import { cookies } from 'next/headers';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { extractSupportedLanguage } from '@documenso/lib/utils/i18n';
export default function middleware(req: NextRequest) {
const lang = extractSupportedLanguage({
headers: req.headers,
cookies: cookies(),
});
const response = NextResponse.next();
response.cookies.set('i18n', lang);
return response;
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - ingest (analytics)
* - site.webmanifest
*/
{
source: '/((?!api|_next/static|_next/image|ingest|favicon|site.webmanifest).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
],
};

View File

@ -1,6 +1,6 @@
{ {
"name": "@documenso/web", "name": "@documenso/web",
"version": "1.7.0-rc.4", "version": "1.7.1-rc.0",
"private": true, "private": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {

View File

@ -2,6 +2,7 @@ declare namespace NodeJS {
export interface ProcessEnv { export interface ProcessEnv {
NEXT_PUBLIC_WEBAPP_URL?: string; NEXT_PUBLIC_WEBAPP_URL?: string;
NEXT_PUBLIC_MARKETING_URL?: string; NEXT_PUBLIC_MARKETING_URL?: string;
NEXT_PRIVATE_INTERNAL_WEBAPP_URL?:string;
NEXT_PRIVATE_DATABASE_URL: string; NEXT_PRIVATE_DATABASE_URL: string;

View File

@ -12,7 +12,6 @@ import {
import { Badge } from '@documenso/ui/primitives/badge'; import { Badge } from '@documenso/ui/primitives/badge';
import { DocumentStatus } from '~/components/formatter/document-status'; import { DocumentStatus } from '~/components/formatter/document-status';
import { LocaleDate } from '~/components/formatter/locale-date';
import { AdminActions } from './admin-actions'; import { AdminActions } from './admin-actions';
import { RecipientItem } from './recipient-item'; import { RecipientItem } from './recipient-item';
@ -25,7 +24,7 @@ type AdminDocumentDetailsPageProps = {
}; };
export default async function AdminDocumentDetailsPage({ params }: AdminDocumentDetailsPageProps) { export default async function AdminDocumentDetailsPage({ params }: AdminDocumentDetailsPageProps) {
setupI18nSSR(); const { i18n } = setupI18nSSR();
const document = await getEntireDocument({ id: Number(params.id) }); const document = await getEntireDocument({ id: Number(params.id) });
@ -46,12 +45,11 @@ export default async function AdminDocumentDetailsPage({ params }: AdminDocument
<div className="text-muted-foreground mt-4 text-sm"> <div className="text-muted-foreground mt-4 text-sm">
<div> <div>
<Trans>Created on</Trans>:{' '} <Trans>Created on</Trans>: {i18n.date(document.createdAt, DateTime.DATETIME_MED)}
<LocaleDate date={document.createdAt} format={DateTime.DATETIME_MED} />
</div> </div>
<div> <div>
<Trans>Last updated at</Trans>:{' '} <Trans>Last updated at</Trans>: {i18n.date(document.updatedAt, DateTime.DATETIME_MED)}
<LocaleDate date={document.updatedAt} format={DateTime.DATETIME_MED} />
</div> </div>
</div> </div>

View File

@ -21,12 +21,11 @@ import { Input } from '@documenso/ui/primitives/input';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { DocumentStatus } from '~/components/formatter/document-status'; import { DocumentStatus } from '~/components/formatter/document-status';
import { LocaleDate } from '~/components/formatter/locale-date';
// export type AdminDocumentResultsProps = {}; // export type AdminDocumentResultsProps = {};
export const AdminDocumentResults = () => { export const AdminDocumentResults = () => {
const { _ } = useLingui(); const { _, i18n } = useLingui();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@ -62,7 +61,7 @@ export const AdminDocumentResults = () => {
{ {
header: _(msg`Created`), header: _(msg`Created`),
accessorKey: 'createdAt', accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />, cell: ({ row }) => i18n.date(row.original.createdAt),
}, },
{ {
header: _(msg`Title`), header: _(msg`Title`),
@ -122,7 +121,7 @@ export const AdminDocumentResults = () => {
{ {
header: 'Last updated', header: 'Last updated',
accessorKey: 'updatedAt', accessorKey: 'updatedAt',
cell: ({ row }) => <LocaleDate date={row.original.updatedAt} />, cell: ({ row }) => i18n.date(row.original.updatedAt),
}, },
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[]; ] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
}, []); }, []);

View File

@ -7,7 +7,6 @@ import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted'; import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
import { useLocale } from '@documenso/lib/client-only/providers/locale';
import type { Document, Recipient, User } from '@documenso/prisma/client'; import type { Document, Recipient, User } from '@documenso/prisma/client';
export type DocumentPageViewInformationProps = { export type DocumentPageViewInformationProps = {
@ -24,21 +23,9 @@ export const DocumentPageViewInformation = ({
}: DocumentPageViewInformationProps) => { }: DocumentPageViewInformationProps) => {
const isMounted = useIsMounted(); const isMounted = useIsMounted();
const { locale } = useLocale(); const { _, i18n } = useLingui();
const { _ } = useLingui();
const documentInformation = useMemo(() => { const documentInformation = useMemo(() => {
let createdValue = DateTime.fromJSDate(document.createdAt).toFormat('MMMM d, yyyy');
let lastModifiedValue = DateTime.fromJSDate(document.updatedAt).toRelative();
if (!isMounted) {
createdValue = DateTime.fromJSDate(document.createdAt)
.setLocale(locale)
.toFormat('MMMM d, yyyy');
lastModifiedValue = DateTime.fromJSDate(document.updatedAt).setLocale(locale).toRelative();
}
return [ return [
{ {
description: msg`Uploaded by`, description: msg`Uploaded by`,
@ -46,15 +33,19 @@ export const DocumentPageViewInformation = ({
}, },
{ {
description: msg`Created`, description: msg`Created`,
value: createdValue, value: DateTime.fromJSDate(document.createdAt)
.setLocale(i18n.locales?.[0] || i18n.locale)
.toFormat('MMMM d, yyyy'),
}, },
{ {
description: msg`Last modified`, description: msg`Last modified`,
value: lastModifiedValue, value: DateTime.fromJSDate(document.updatedAt)
.setLocale(i18n.locales?.[0] || i18n.locale)
.toRelative(),
}, },
]; ];
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMounted, document, locale, userId]); }, [isMounted, document, userId]);
return ( return (
<section className="dark:bg-background text-foreground border-border bg-widget flex flex-col rounded-xl border"> <section className="dark:bg-background text-foreground border-border bg-widget flex flex-col rounded-xl border">

View File

@ -20,8 +20,6 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton'; import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table'; import { TableCell } from '@documenso/ui/primitives/table';
import { LocaleDate } from '~/components/formatter/locale-date';
export type DocumentLogsDataTableProps = { export type DocumentLogsDataTableProps = {
documentId: number; documentId: number;
}; };
@ -32,7 +30,7 @@ const dateFormat: DateTimeFormatOptions = {
}; };
export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps) => { export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps) => {
const { _ } = useLingui(); const { _, i18n } = useLingui();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams(); const updateSearchParams = useUpdateSearchParams();
@ -78,7 +76,7 @@ export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps
{ {
header: _(msg`Time`), header: _(msg`Time`),
accessorKey: 'createdAt', accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate format={dateFormat} date={row.original.createdAt} />, cell: ({ row }) => i18n.date(row.original.createdAt, dateFormat),
}, },
{ {
header: _(msg`User`), header: _(msg`User`),

View File

@ -9,7 +9,6 @@ import { DateTime } from 'luxon';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getLocale } from '@documenso/lib/server-only/headers/get-locale';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import type { Recipient, Team } from '@documenso/prisma/client'; import type { Recipient, Team } from '@documenso/prisma/client';
@ -32,9 +31,7 @@ export type DocumentLogsPageViewProps = {
}; };
export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageViewProps) => { export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageViewProps) => {
const { _ } = useLingui(); const { _, i18n } = useLingui();
const locale = getLocale();
const { id } = params; const { id } = params;
@ -87,13 +84,13 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
{ {
description: msg`Date created`, description: msg`Date created`,
value: DateTime.fromJSDate(document.createdAt) value: DateTime.fromJSDate(document.createdAt)
.setLocale(locale) .setLocale(i18n.locales?.[0] || i18n.locale)
.toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS), .toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS),
}, },
{ {
description: msg`Last updated`, description: msg`Last updated`,
value: DateTime.fromJSDate(document.updatedAt) value: DateTime.fromJSDate(document.updatedAt)
.setLocale(locale) .setLocale(i18n.locales?.[0] || i18n.locale)
.toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS), .toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS),
}, },
{ {

View File

@ -18,7 +18,6 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip'; import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
import { DocumentStatus } from '~/components/formatter/document-status'; import { DocumentStatus } from '~/components/formatter/document-status';
import { LocaleDate } from '~/components/formatter/locale-date';
import { DataTableActionButton } from './data-table-action-button'; import { DataTableActionButton } from './data-table-action-button';
import { DataTableActionDropdown } from './data-table-action-dropdown'; import { DataTableActionDropdown } from './data-table-action-dropdown';
@ -41,8 +40,9 @@ export const DocumentsDataTable = ({
showSenderColumn, showSenderColumn,
team, team,
}: DocumentsDataTableProps) => { }: DocumentsDataTableProps) => {
const { _, i18n } = useLingui();
const { data: session } = useSession(); const { data: session } = useSession();
const { _ } = useLingui();
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
@ -53,12 +53,8 @@ export const DocumentsDataTable = ({
{ {
header: _(msg`Created`), header: _(msg`Created`),
accessorKey: 'createdAt', accessorKey: 'createdAt',
cell: ({ row }) => ( cell: ({ row }) =>
<LocaleDate i18n.date(row.original.createdAt, { ...DateTime.DATETIME_SHORT, hourCycle: 'h12' }),
date={row.original.createdAt}
format={{ ...DateTime.DATETIME_SHORT, hourCycle: 'h12' }}
/>
),
}, },
{ {
header: _(msg`Title`), header: _(msg`Title`),

View File

@ -16,8 +16,6 @@ import { type Stripe } from '@documenso/lib/server-only/stripe';
import { getSubscriptionsByUserId } from '@documenso/lib/server-only/subscription/get-subscriptions-by-user-id'; import { getSubscriptionsByUserId } from '@documenso/lib/server-only/subscription/get-subscriptions-by-user-id';
import { SubscriptionStatus } from '@documenso/prisma/client'; import { SubscriptionStatus } from '@documenso/prisma/client';
import { LocaleDate } from '~/components/formatter/locale-date';
import { BillingPlans } from './billing-plans'; import { BillingPlans } from './billing-plans';
import { BillingPortalButton } from './billing-portal-button'; import { BillingPortalButton } from './billing-portal-button';
@ -26,7 +24,7 @@ export const metadata: Metadata = {
}; };
export default async function BillingSettingsPage() { export default async function BillingSettingsPage() {
setupI18nSSR(); const { i18n } = setupI18nSSR();
let { user } = await getRequiredServerComponentSession(); let { user } = await getRequiredServerComponentSession();
@ -104,12 +102,12 @@ export default async function BillingSettingsPage() {
{subscription.cancelAtPeriodEnd ? ( {subscription.cancelAtPeriodEnd ? (
<span> <span>
end on{' '} end on{' '}
<LocaleDate className="font-semibold" date={subscription.periodEnd} />. <span className="font-semibold">{i18n.date(subscription.periodEnd)}.</span>
</span> </span>
) : ( ) : (
<span> <span>
automatically renew on{' '} automatically renew on{' '}
<LocaleDate className="font-semibold" date={subscription.periodEnd} />. <span className="font-semibold">{i18n.date(subscription.periodEnd)}.</span>
</span> </span>
)} )}
</span> </span>

View File

@ -20,15 +20,13 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton'; import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table'; import { TableCell } from '@documenso/ui/primitives/table';
import { LocaleDate } from '~/components/formatter/locale-date';
const dateFormat: DateTimeFormatOptions = { const dateFormat: DateTimeFormatOptions = {
...DateTime.DATETIME_SHORT, ...DateTime.DATETIME_SHORT,
hourCycle: 'h12', hourCycle: 'h12',
}; };
export const UserSecurityActivityDataTable = () => { export const UserSecurityActivityDataTable = () => {
const { _ } = useLingui(); const { _, i18n } = useLingui();
const pathname = usePathname(); const pathname = usePathname();
const router = useRouter(); const router = useRouter();
@ -71,7 +69,7 @@ export const UserSecurityActivityDataTable = () => {
{ {
header: _(msg`Date`), header: _(msg`Date`),
accessorKey: 'createdAt', accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate format={dateFormat} date={row.original.createdAt} />, cell: ({ row }) => i18n.date(row.original.createdAt, dateFormat),
}, },
{ {
header: _(msg`Device`), header: _(msg`Device`),

View File

@ -7,11 +7,10 @@ import { getUserTokens } from '@documenso/lib/server-only/public-api/get-all-use
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import DeleteTokenDialog from '~/components/(dashboard)/settings/token/delete-token-dialog'; import DeleteTokenDialog from '~/components/(dashboard)/settings/token/delete-token-dialog';
import { LocaleDate } from '~/components/formatter/locale-date';
import { ApiTokenForm } from '~/components/forms/token'; import { ApiTokenForm } from '~/components/forms/token';
export default async function ApiTokensPage() { export default async function ApiTokensPage() {
setupI18nSSR(); const { i18n } = setupI18nSSR();
const { user } = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
@ -65,13 +64,11 @@ export default async function ApiTokensPage() {
<h5 className="text-base">{token.name}</h5> <h5 className="text-base">{token.name}</h5>
<p className="text-muted-foreground mt-2 text-xs"> <p className="text-muted-foreground mt-2 text-xs">
<Trans>Created on</Trans>{' '} <Trans>Created on {i18n.date(token.createdAt, DateTime.DATETIME_FULL)}</Trans>
<LocaleDate date={token.createdAt} format={DateTime.DATETIME_FULL} />
</p> </p>
{token.expires ? ( {token.expires ? (
<p className="text-muted-foreground mt-1 text-xs"> <p className="text-muted-foreground mt-1 text-xs">
<Trans>Expires on</Trans>{' '} <Trans>Expires on {i18n.date(token.expires, DateTime.DATETIME_FULL)}</Trans>
<LocaleDate date={token.expires} format={DateTime.DATETIME_FULL} />
</p> </p>
) : ( ) : (
<p className="text-muted-foreground mt-1 text-xs"> <p className="text-muted-foreground mt-1 text-xs">

View File

@ -16,10 +16,9 @@ import { Button } from '@documenso/ui/primitives/button';
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header'; import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
import { CreateWebhookDialog } from '~/components/(dashboard)/settings/webhooks/create-webhook-dialog'; import { CreateWebhookDialog } from '~/components/(dashboard)/settings/webhooks/create-webhook-dialog';
import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog'; import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog';
import { LocaleDate } from '~/components/formatter/locale-date';
export default function WebhookPage() { export default function WebhookPage() {
const { _ } = useLingui(); const { _, i18n } = useLingui();
const { data: webhooks, isLoading } = trpc.webhook.getWebhooks.useQuery(); const { data: webhooks, isLoading } = trpc.webhook.getWebhooks.useQuery();
@ -86,10 +85,7 @@ export default function WebhookPage() {
</p> </p>
<p className="text-muted-foreground mt-2 text-xs"> <p className="text-muted-foreground mt-2 text-xs">
<Trans> <Trans>Created on {i18n.date(webhook.createdAt, DateTime.DATETIME_FULL)}</Trans>
Created on{' '}
<LocaleDate date={webhook.createdAt} format={DateTime.DATETIME_FULL} />
</Trans>
</p> </p>
</div> </div>

View File

@ -17,7 +17,6 @@ import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { LocaleDate } from '~/components/formatter/locale-date';
import { TemplateType } from '~/components/formatter/template-type'; import { TemplateType } from '~/components/formatter/template-type';
import { DataTableActionDropdown } from './data-table-action-dropdown'; import { DataTableActionDropdown } from './data-table-action-dropdown';
@ -48,7 +47,7 @@ export const TemplatesDataTable = ({
const updateSearchParams = useUpdateSearchParams(); const updateSearchParams = useUpdateSearchParams();
const { _ } = useLingui(); const { _, i18n } = useLingui();
const { remaining } = useLimits(); const { remaining } = useLimits();
const columns = useMemo(() => { const columns = useMemo(() => {
@ -56,7 +55,7 @@ export const TemplatesDataTable = ({
{ {
header: _(msg`Created`), header: _(msg`Created`),
accessorKey: 'createdAt', accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />, cell: ({ row }) => i18n.date(row.original.createdAt),
}, },
{ {
header: _(msg`Title`), header: _(msg`Title`),

View File

@ -4,6 +4,7 @@ import { DateTime } from 'luxon';
import type { DateTimeFormatOptions } from 'luxon'; import type { DateTimeFormatOptions } from 'luxon';
import { UAParser } from 'ua-parser-js'; import { UAParser } from 'ua-parser-js';
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
import type { TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs'; import type { TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs';
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs'; import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
import { import {
@ -15,8 +16,6 @@ import {
TableRow, TableRow,
} from '@documenso/ui/primitives/table'; } from '@documenso/ui/primitives/table';
import { LocaleDate } from '~/components/formatter/locale-date';
export type AuditLogDataTableProps = { export type AuditLogDataTableProps = {
logs: TDocumentAuditLog[]; logs: TDocumentAuditLog[];
}; };
@ -49,7 +48,9 @@ export const AuditLogDataTable = ({ logs }: AuditLogDataTableProps) => {
{logs.map((log, i) => ( {logs.map((log, i) => (
<TableRow className="break-inside-avoid" key={i}> <TableRow className="break-inside-avoid" key={i}>
<TableCell> <TableCell>
<LocaleDate format={dateFormat} date={log.createdAt} /> {DateTime.fromJSDate(log.createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toLocaleString(dateFormat)}
</TableCell> </TableCell>
<TableCell> <TableCell>

View File

@ -2,7 +2,9 @@ import React from 'react';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server'; import { DateTime } from 'luxon';
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
import { RECIPIENT_ROLES_DESCRIPTION_ENG } from '@documenso/lib/constants/recipient-roles'; import { RECIPIENT_ROLES_DESCRIPTION_ENG } from '@documenso/lib/constants/recipient-roles';
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document'; import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt'; import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
@ -10,7 +12,6 @@ import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { Card, CardContent } from '@documenso/ui/primitives/card';
import { Logo } from '~/components/branding/logo'; import { Logo } from '~/components/branding/logo';
import { LocaleDate } from '~/components/formatter/locale-date';
import { AuditLogDataTable } from './data-table'; import { AuditLogDataTable } from './data-table';
@ -21,8 +22,6 @@ type AuditLogProps = {
}; };
export default async function AuditLog({ searchParams }: AuditLogProps) { export default async function AuditLog({ searchParams }: AuditLogProps) {
setupI18nSSR();
const { d } = searchParams; const { d } = searchParams;
if (typeof d !== 'string' || !d) { if (typeof d !== 'string' || !d) {
@ -89,7 +88,9 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
<span className="font-medium">Created At</span> <span className="font-medium">Created At</span>
<span className="mt-1 block"> <span className="mt-1 block">
<LocaleDate date={document.createdAt} format="yyyy-mm-dd hh:mm:ss a (ZZZZ)" /> {DateTime.fromJSDate(document.createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-mm-dd hh:mm:ss a (ZZZZ)')}
</span> </span>
</p> </p>
@ -97,7 +98,9 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
<span className="font-medium">Last Updated</span> <span className="font-medium">Last Updated</span>
<span className="mt-1 block"> <span className="mt-1 block">
<LocaleDate date={document.updatedAt} format="yyyy-mm-dd hh:mm:ss a (ZZZZ)" /> {DateTime.fromJSDate(document.updatedAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-mm-dd hh:mm:ss a (ZZZZ)')}
</span> </span>
</p> </p>

View File

@ -2,10 +2,11 @@ import React from 'react';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { UAParser } from 'ua-parser-js'; import { UAParser } from 'ua-parser-js';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server'; import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
import { import {
RECIPIENT_ROLES_DESCRIPTION_ENG, RECIPIENT_ROLES_DESCRIPTION_ENG,
RECIPIENT_ROLE_SIGNING_REASONS_ENG, RECIPIENT_ROLE_SIGNING_REASONS_ENG,
@ -27,7 +28,6 @@ import {
} from '@documenso/ui/primitives/table'; } from '@documenso/ui/primitives/table';
import { Logo } from '~/components/branding/logo'; import { Logo } from '~/components/branding/logo';
import { LocaleDate } from '~/components/formatter/locale-date';
type SigningCertificateProps = { type SigningCertificateProps = {
searchParams: { searchParams: {
@ -41,8 +41,6 @@ const FRIENDLY_SIGNING_REASONS = {
}; };
export default async function SigningCertificate({ searchParams }: SigningCertificateProps) { export default async function SigningCertificate({ searchParams }: SigningCertificateProps) {
setupI18nSSR();
const { d } = searchParams; const { d } = searchParams;
if (typeof d !== 'string' || !d) { if (typeof d !== 'string' || !d) {
@ -231,42 +229,33 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
<p className="text-muted-foreground text-sm print:text-xs"> <p className="text-muted-foreground text-sm print:text-xs">
<span className="font-medium">Sent:</span>{' '} <span className="font-medium">Sent:</span>{' '}
<span className="inline-block"> <span className="inline-block">
{logs.EMAIL_SENT[0] ? ( {logs.EMAIL_SENT[0]
<LocaleDate ? DateTime.fromJSDate(logs.EMAIL_SENT[0].createdAt)
date={logs.EMAIL_SENT[0].createdAt} .setLocale(APP_I18N_OPTIONS.defaultLocale)
format="yyyy-MM-dd hh:mm:ss a (ZZZZ)" .toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
/> : 'Unknown'}
) : (
'Unknown'
)}
</span> </span>
</p> </p>
<p className="text-muted-foreground text-sm print:text-xs"> <p className="text-muted-foreground text-sm print:text-xs">
<span className="font-medium">Viewed:</span>{' '} <span className="font-medium">Viewed:</span>{' '}
<span className="inline-block"> <span className="inline-block">
{logs.DOCUMENT_OPENED[0] ? ( {logs.DOCUMENT_OPENED[0]
<LocaleDate ? DateTime.fromJSDate(logs.DOCUMENT_OPENED[0].createdAt)
date={logs.DOCUMENT_OPENED[0].createdAt} .setLocale(APP_I18N_OPTIONS.defaultLocale)
format="yyyy-MM-dd hh:mm:ss a (ZZZZ)" .toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
/> : 'Unknown'}
) : (
'Unknown'
)}
</span> </span>
</p> </p>
<p className="text-muted-foreground text-sm print:text-xs"> <p className="text-muted-foreground text-sm print:text-xs">
<span className="font-medium">Signed:</span>{' '} <span className="font-medium">Signed:</span>{' '}
<span className="inline-block"> <span className="inline-block">
{logs.DOCUMENT_RECIPIENT_COMPLETED[0] ? ( {logs.DOCUMENT_RECIPIENT_COMPLETED[0]
<LocaleDate ? DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt)
date={logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt} .setLocale(APP_I18N_OPTIONS.defaultLocale)
format="yyyy-MM-dd hh:mm:ss a (ZZZZ)" .toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
/> : 'Unknown'}
) : (
'Unknown'
)}
</span> </span>
</p> </p>

View File

@ -127,7 +127,7 @@ export const DropdownField = ({
await removeSignedFieldWithToken(payload); await removeSignedFieldWithToken(payload);
} }
setLocalChoice(parsedFieldMeta.defaultValue ?? ''); setLocalChoice('');
startTransition(() => router.refresh()); startTransition(() => router.refresh());
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -179,7 +179,7 @@ export const DropdownField = ({
{!field.inserted && ( {!field.inserted && (
<p className="group-hover:text-primary text-muted-foreground flex flex-col items-center justify-center duration-200"> <p className="group-hover:text-primary text-muted-foreground flex flex-col items-center justify-center duration-200">
<Select value={parsedFieldMeta.defaultValue} onValueChange={handleSelectItem}> <Select value={localChoice} onValueChange={handleSelectItem}>
<SelectTrigger <SelectTrigger
className={cn( className={cn(
'text-muted-foreground z-10 h-full w-full border-none ring-0 focus:ring-0', 'text-muted-foreground z-10 h-full w-full border-none ring-0 focus:ring-0',
@ -189,7 +189,7 @@ export const DropdownField = ({
}, },
)} )}
> >
<SelectValue placeholder={`-- ${_(msg`Select`)} --`} /> <SelectValue placeholder={`${_(msg`Select`)}`} />
</SelectTrigger> </SelectTrigger>
<SelectContent className="w-full ring-0 focus:ring-0" position="popper"> <SelectContent className="w-full ring-0 focus:ring-0" position="popper">
{parsedFieldMeta?.values?.map((item, index) => ( {parsedFieldMeta?.values?.map((item, index) => (

View File

@ -12,7 +12,6 @@ import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import DeleteTokenDialog from '~/components/(dashboard)/settings/token/delete-token-dialog'; import DeleteTokenDialog from '~/components/(dashboard)/settings/token/delete-token-dialog';
import { LocaleDate } from '~/components/formatter/locale-date';
import { ApiTokenForm } from '~/components/forms/token'; import { ApiTokenForm } from '~/components/forms/token';
type ApiTokensPageProps = { type ApiTokensPageProps = {
@ -22,7 +21,7 @@ type ApiTokensPageProps = {
}; };
export default async function ApiTokensPage({ params }: ApiTokensPageProps) { export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
setupI18nSSR(); const { i18n } = setupI18nSSR();
const { teamUrl } = params; const { teamUrl } = params;
@ -98,13 +97,17 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
<h5 className="text-base">{token.name}</h5> <h5 className="text-base">{token.name}</h5>
<p className="text-muted-foreground mt-2 text-xs"> <p className="text-muted-foreground mt-2 text-xs">
<Trans>Created on</Trans>{' '} <Trans>
<LocaleDate date={token.createdAt} format={DateTime.DATETIME_FULL} /> Created on
{i18n.date(token.createdAt, DateTime.DATETIME_FULL)}
</Trans>
</p> </p>
{token.expires ? ( {token.expires ? (
<p className="text-muted-foreground mt-1 text-xs"> <p className="text-muted-foreground mt-1 text-xs">
<Trans>Expires on</Trans>{' '} <Trans>
<LocaleDate date={token.expires} format={DateTime.DATETIME_FULL} /> Expires on
{i18n.date(token.expires, DateTime.DATETIME_FULL)}
</Trans>
</p> </p>
) : ( ) : (
<p className="text-muted-foreground mt-1 text-xs"> <p className="text-muted-foreground mt-1 text-xs">

View File

@ -16,11 +16,10 @@ import { Button } from '@documenso/ui/primitives/button';
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header'; import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
import { CreateWebhookDialog } from '~/components/(dashboard)/settings/webhooks/create-webhook-dialog'; import { CreateWebhookDialog } from '~/components/(dashboard)/settings/webhooks/create-webhook-dialog';
import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog'; import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog';
import { LocaleDate } from '~/components/formatter/locale-date';
import { useCurrentTeam } from '~/providers/team'; import { useCurrentTeam } from '~/providers/team';
export default function WebhookPage() { export default function WebhookPage() {
const { _ } = useLingui(); const { _, i18n } = useLingui();
const team = useCurrentTeam(); const team = useCurrentTeam();
@ -91,10 +90,7 @@ export default function WebhookPage() {
</p> </p>
<p className="text-muted-foreground mt-2 text-xs"> <p className="text-muted-foreground mt-2 text-xs">
<Trans> <Trans>Created on {i18n.date(webhook.createdAt, DateTime.DATETIME_FULL)}</Trans>
Created on{' '}
<LocaleDate date={webhook.createdAt} format={DateTime.DATETIME_FULL} />
</Trans>
</p> </p>
</div> </div>

View File

@ -1,7 +1,6 @@
import { Suspense } from 'react'; import { Suspense } from 'react';
import { Caveat, Inter } from 'next/font/google'; import { Caveat, Inter } from 'next/font/google';
import { cookies, headers } from 'next/headers';
import { AxiomWebVitals } from 'next-axiom'; import { AxiomWebVitals } from 'next-axiom';
import { PublicEnvScript } from 'next-runtime-env'; import { PublicEnvScript } from 'next-runtime-env';
@ -9,12 +8,8 @@ import { PublicEnvScript } from 'next-runtime-env';
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag'; import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
import { I18nClientProvider } from '@documenso/lib/client-only/providers/i18n.client'; import { I18nClientProvider } from '@documenso/lib/client-only/providers/i18n.client';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server'; import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { LocaleProvider } from '@documenso/lib/client-only/providers/locale';
import { IS_APP_WEB_I18N_ENABLED, NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { IS_APP_WEB_I18N_ENABLED, NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import type { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n';
import { getServerComponentAllFlags } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag'; import { getServerComponentAllFlags } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
import { getLocale } from '@documenso/lib/server-only/headers/get-locale';
import { TrpcProvider } from '@documenso/trpc/react'; import { TrpcProvider } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Toaster } from '@documenso/ui/primitives/toaster'; import { Toaster } from '@documenso/ui/primitives/toaster';
@ -61,32 +56,7 @@ export function generateMetadata() {
export default async function RootLayout({ children }: { children: React.ReactNode }) { export default async function RootLayout({ children }: { children: React.ReactNode }) {
const flags = await getServerComponentAllFlags(); const flags = await getServerComponentAllFlags();
const locale = getLocale(); const { i18n, lang, locales } = setupI18nSSR();
let overrideLang: (typeof SUPPORTED_LANGUAGE_CODES)[number] | undefined;
// Should be safe to remove when we upgrade NextJS.
// https://github.com/vercel/next.js/pull/65008
// Currently if the middleware sets the cookie, it's not accessible in the cookies
// during the same render.
// So we go the roundabout way of checking the header for the set-cookie value.
if (!cookies().get('i18n')) {
const setCookieValue = headers().get('set-cookie');
const i18nCookie = setCookieValue?.split(';').find((cookie) => cookie.startsWith('i18n='));
if (i18nCookie) {
const i18n = i18nCookie.split('=')[1];
overrideLang = ZSupportedLanguageCodeSchema.parse(i18n);
}
}
// Disable i18n for now until we get translations.
if (!IS_APP_WEB_I18N_ENABLED) {
overrideLang = 'en';
}
const { lang, i18n } = setupI18nSSR(overrideLang);
return ( return (
<html <html
@ -110,21 +80,22 @@ export default async function RootLayout({ children }: { children: React.ReactNo
</Suspense> </Suspense>
<body> <body>
<LocaleProvider locale={locale}> <FeatureFlagProvider initialFlags={flags}>
<FeatureFlagProvider initialFlags={flags}> <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem> <TooltipProvider>
<TooltipProvider> <TrpcProvider>
<TrpcProvider> <I18nClientProvider
<I18nClientProvider initialLocale={lang} initialMessages={i18n.messages}> initialLocaleData={{ lang, locales }}
{children} initialMessages={i18n.messages}
</I18nClientProvider> >
</TrpcProvider> {children}
</TooltipProvider> </I18nClientProvider>
</ThemeProvider> </TrpcProvider>
</TooltipProvider>
</ThemeProvider>
<Toaster /> <Toaster />
</FeatureFlagProvider> </FeatureFlagProvider>
</LocaleProvider>
</body> </body>
</html> </html>
); );

View File

@ -7,10 +7,11 @@ import { useRouter } from 'next/navigation';
import type { MessageDescriptor } from '@lingui/core'; import type { MessageDescriptor } from '@lingui/core';
import { Trans, msg } from '@lingui/macro'; import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Loader, Monitor, Moon, Sun } from 'lucide-react'; import { CheckIcon, Loader, Monitor, Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { import {
DOCUMENTS_PAGE_SHORTCUT, DOCUMENTS_PAGE_SHORTCUT,
SETTINGS_PAGE_SHORTCUT, SETTINGS_PAGE_SHORTCUT,
@ -20,7 +21,10 @@ import {
DO_NOT_INVALIDATE_QUERY_ON_MUTATION, DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
SKIP_QUERY_BATCH_META, SKIP_QUERY_BATCH_META,
} from '@documenso/lib/constants/trpc'; } from '@documenso/lib/constants/trpc';
import { switchI18NLanguage } from '@documenso/lib/server-only/i18n/switch-i18n-language';
import { dynamicActivate } from '@documenso/lib/utils/i18n';
import { trpc as trpcReact } from '@documenso/trpc/react'; import { trpc as trpcReact } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { import {
CommandDialog, CommandDialog,
CommandEmpty, CommandEmpty,
@ -31,6 +35,7 @@ import {
CommandShortcut, CommandShortcut,
} from '@documenso/ui/primitives/command'; } from '@documenso/ui/primitives/command';
import { THEMES_TYPE } from '@documenso/ui/primitives/constants'; import { THEMES_TYPE } from '@documenso/ui/primitives/constants';
import { useToast } from '@documenso/ui/primitives/use-toast';
const DOCUMENTS_PAGES = [ const DOCUMENTS_PAGES = [
{ {
@ -207,6 +212,9 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
<Commands push={push} pages={SETTINGS_PAGES} /> <Commands push={push} pages={SETTINGS_PAGES} />
</CommandGroup> </CommandGroup>
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Preferences`)}> <CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Preferences`)}>
<CommandItem className="-mx-2 -my-1 rounded-lg" onSelect={() => addPage('language')}>
Change language
</CommandItem>
<CommandItem className="-mx-2 -my-1 rounded-lg" onSelect={() => addPage('theme')}> <CommandItem className="-mx-2 -my-1 rounded-lg" onSelect={() => addPage('theme')}>
Change theme Change theme
</CommandItem> </CommandItem>
@ -218,7 +226,9 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
)} )}
</> </>
)} )}
{currentPage === 'theme' && <ThemeCommands setTheme={setTheme} />} {currentPage === 'theme' && <ThemeCommands setTheme={setTheme} />}
{currentPage === 'language' && <LanguageCommands />}
</CommandList> </CommandList>
</CommandDialog> </CommandDialog>
); );
@ -269,3 +279,46 @@ const ThemeCommands = ({ setTheme }: { setTheme: (_theme: string) => void }) =>
</CommandItem> </CommandItem>
)); ));
}; };
const LanguageCommands = () => {
const { i18n, _ } = useLingui();
const { toast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const setLanguage = async (lang: string) => {
if (isLoading || lang === i18n.locale) {
return;
}
setIsLoading(true);
try {
await dynamicActivate(i18n, lang);
await switchI18NLanguage(lang);
} catch (err) {
toast({
title: _(msg`An unknown error occurred`),
variant: 'destructive',
description: _(msg`Unable to change the language at this time. Please try again later.`),
});
}
setIsLoading(false);
};
return Object.values(SUPPORTED_LANGUAGES).map((language) => (
<CommandItem
disabled={isLoading}
key={language.full}
onSelect={async () => setLanguage(language.short)}
className="-my-1 mx-2 rounded-lg first:mt-2 last:mb-2"
>
<CheckIcon
className={cn('mr-2 h-4 w-4', i18n.locale === language.short ? 'opacity-100' : 'opacity-0')}
/>
{language.full}
</CommandItem>
));
};

View File

@ -1,5 +1,7 @@
'use client'; 'use client';
import { useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
@ -17,6 +19,7 @@ import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { canExecuteTeamAction } from '@documenso/lib/utils/teams'; import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
import type { User } from '@documenso/prisma/client'; import type { User } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { LanguageSwitcherDialog } from '@documenso/ui/components/common/language-switcher-dialog';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { AvatarWithText } from '@documenso/ui/primitives/avatar'; import { AvatarWithText } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -41,6 +44,8 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
const pathname = usePathname(); const pathname = usePathname();
const [languageSwitcherOpen, setLanguageSwitcherOpen] = useState(false);
const isUserAdmin = isAdmin(user); const isUserAdmin = isAdmin(user);
const { data: teamsQueryResult } = trpc.team.getTeams.useQuery(undefined, { const { data: teamsQueryResult } = trpc.team.getTeams.useQuery(undefined, {
@ -274,6 +279,13 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
</DropdownMenuItem> </DropdownMenuItem>
)} )}
<DropdownMenuItem
className="text-muted-foreground px-4 py-2"
onClick={() => setLanguageSwitcherOpen(true)}
>
<Trans>Language</Trans>
</DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
className="text-destructive/90 hover:!text-destructive px-4 py-2" className="text-destructive/90 hover:!text-destructive px-4 py-2"
onSelect={async () => onSelect={async () =>
@ -285,6 +297,8 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
<Trans>Sign Out</Trans> <Trans>Sign Out</Trans>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
<LanguageSwitcherDialog open={languageSwitcherOpen} setOpen={setLanguageSwitcherOpen} />
</DropdownMenu> </DropdownMenu>
); );
}; };

View File

@ -22,12 +22,10 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton'; import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table'; import { TableCell } from '@documenso/ui/primitives/table';
import { LocaleDate } from '~/components/formatter/locale-date';
import { LeaveTeamDialog } from '../dialogs/leave-team-dialog'; import { LeaveTeamDialog } from '../dialogs/leave-team-dialog';
export const CurrentUserTeamsDataTable = () => { export const CurrentUserTeamsDataTable = () => {
const { _ } = useLingui(); const { _, i18n } = useLingui();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams(); const updateSearchParams = useUpdateSearchParams();
@ -91,7 +89,7 @@ export const CurrentUserTeamsDataTable = () => {
{ {
header: _(msg`Member Since`), header: _(msg`Member Since`),
accessorKey: 'createdAt', accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />, cell: ({ row }) => i18n.date(row.original.createdAt),
}, },
{ {
id: 'actions', id: 'actions',

View File

@ -18,13 +18,11 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton'; import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table'; import { TableCell } from '@documenso/ui/primitives/table';
import { LocaleDate } from '~/components/formatter/locale-date';
import { CreateTeamCheckoutDialog } from '../dialogs/create-team-checkout-dialog'; import { CreateTeamCheckoutDialog } from '../dialogs/create-team-checkout-dialog';
import { PendingUserTeamsDataTableActions } from './pending-user-teams-data-table-actions'; import { PendingUserTeamsDataTableActions } from './pending-user-teams-data-table-actions';
export const PendingUserTeamsDataTable = () => { export const PendingUserTeamsDataTable = () => {
const { _ } = useLingui(); const { _, i18n } = useLingui();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams(); const updateSearchParams = useUpdateSearchParams();
@ -79,7 +77,7 @@ export const PendingUserTeamsDataTable = () => {
{ {
header: _(msg`Created on`), header: _(msg`Created on`),
accessorKey: 'createdAt', accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />, cell: ({ row }) => i18n.date(row.original.createdAt),
}, },
{ {
id: 'actions', id: 'actions',

View File

@ -27,8 +27,6 @@ import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table'; import { TableCell } from '@documenso/ui/primitives/table';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { LocaleDate } from '~/components/formatter/locale-date';
export type TeamMemberInvitesDataTableProps = { export type TeamMemberInvitesDataTableProps = {
teamId: number; teamId: number;
}; };
@ -37,7 +35,7 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams(); const updateSearchParams = useUpdateSearchParams();
const { _ } = useLingui(); const { _, i18n } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse( const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
@ -129,7 +127,7 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
{ {
header: _(msg`Invited At`), header: _(msg`Invited At`),
accessorKey: 'createdAt', accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />, cell: ({ row }) => i18n.date(row.original.createdAt),
}, },
{ {
header: _(msg`Actions`), header: _(msg`Actions`),

View File

@ -29,8 +29,6 @@ import {
import { Skeleton } from '@documenso/ui/primitives/skeleton'; import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table'; import { TableCell } from '@documenso/ui/primitives/table';
import { LocaleDate } from '~/components/formatter/locale-date';
import { DeleteTeamMemberDialog } from '../dialogs/delete-team-member-dialog'; import { DeleteTeamMemberDialog } from '../dialogs/delete-team-member-dialog';
import { UpdateTeamMemberDialog } from '../dialogs/update-team-member-dialog'; import { UpdateTeamMemberDialog } from '../dialogs/update-team-member-dialog';
@ -47,7 +45,7 @@ export const TeamMembersDataTable = ({
teamId, teamId,
teamName, teamName,
}: TeamMembersDataTableProps) => { }: TeamMembersDataTableProps) => {
const { _ } = useLingui(); const { _, i18n } = useLingui();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams(); const updateSearchParams = useUpdateSearchParams();
@ -114,7 +112,7 @@ export const TeamMembersDataTable = ({
{ {
header: _(msg`Member Since`), header: _(msg`Member Since`),
accessorKey: 'createdAt', accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />, cell: ({ row }) => i18n.date(row.original.createdAt),
}, },
{ {
header: _(msg`Actions`), header: _(msg`Actions`),

View File

@ -3,7 +3,9 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { Trans } from '@lingui/macro'; import { Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { ArrowRightIcon, Loader } from 'lucide-react'; import { ArrowRightIcon, Loader } from 'lucide-react';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { UAParser } from 'ua-parser-js'; import { UAParser } from 'ua-parser-js';
@ -18,8 +20,6 @@ import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { Sheet, SheetContent, SheetTrigger } from '@documenso/ui/primitives/sheet'; import { Sheet, SheetContent, SheetTrigger } from '@documenso/ui/primitives/sheet';
import { LocaleDate } from '~/components/formatter/locale-date';
import { DocumentHistorySheetChanges } from './document-history-sheet-changes'; import { DocumentHistorySheetChanges } from './document-history-sheet-changes';
export type DocumentHistorySheetProps = { export type DocumentHistorySheetProps = {
@ -37,6 +37,8 @@ export const DocumentHistorySheet = ({
onMenuOpenChange, onMenuOpenChange,
children, children,
}: DocumentHistorySheetProps) => { }: DocumentHistorySheetProps) => {
const { i18n } = useLingui();
const [isUserDetailsVisible, setIsUserDetailsVisible] = useState(false); const [isUserDetailsVisible, setIsUserDetailsVisible] = useState(false);
const { const {
@ -153,7 +155,9 @@ export const DocumentHistorySheet = ({
{formatDocumentAuditLogActionString(auditLog, userId)} {formatDocumentAuditLogActionString(auditLog, userId)}
</p> </p>
<p className="text-foreground/50 text-xs"> <p className="text-foreground/50 text-xs">
<LocaleDate date={auditLog.createdAt} format="d MMM, yyyy HH:MM a" /> {DateTime.fromJSDate(auditLog.createdAt)
.setLocale(i18n.locales?.[0] || i18n.locale)
.toFormat('d MMM, yyyy HH:MM a')}
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,49 +0,0 @@
'use client';
import type { HTMLAttributes } from 'react';
import { useCallback, useEffect, useState } from 'react';
import type { DateTimeFormatOptions } from 'luxon';
import { DateTime } from 'luxon';
import { useLocale } from '@documenso/lib/client-only/providers/locale';
export type LocaleDateProps = HTMLAttributes<HTMLSpanElement> & {
date: string | number | Date;
format?: DateTimeFormatOptions | string;
};
/**
* Formats the date based on the user locale.
*
* Will use the estimated locale from the user headers on SSR, then will use
* the client browser locale once mounted.
*/
export const LocaleDate = ({ className, date, format, ...props }: LocaleDateProps) => {
const { locale } = useLocale();
const formatDateTime = useCallback(
(date: DateTime) => {
if (typeof format === 'string') {
return date.toFormat(format);
}
return date.toLocaleString(format);
},
[format],
);
const [localeDate, setLocaleDate] = useState(() =>
formatDateTime(DateTime.fromJSDate(new Date(date)).setLocale(locale)),
);
useEffect(() => {
setLocaleDate(formatDateTime(DateTime.fromJSDate(new Date(date))));
}, [date, format, formatDateTime]);
return (
<span className={className} {...props}>
{localeDate}
</span>
);
};

View File

@ -52,8 +52,6 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team'; import { useOptionalCurrentTeam } from '~/providers/team';
import { LocaleDate } from '../formatter/locale-date';
export type ManagePublicTemplateDialogProps = { export type ManagePublicTemplateDialogProps = {
directTemplates: (Template & { directTemplates: (Template & {
directLink: Pick<TemplateDirectLink, 'token' | 'enabled'>; directLink: Pick<TemplateDirectLink, 'token' | 'enabled'>;
@ -93,7 +91,7 @@ export const ManagePublicTemplateDialog = ({
onIsOpenChange, onIsOpenChange,
...props ...props
}: ManagePublicTemplateDialogProps) => { }: ManagePublicTemplateDialogProps) => {
const { _ } = useLingui(); const { _, i18n } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const [open, onOpenChange] = useState(isOpen); const [open, onOpenChange] = useState(isOpen);
@ -300,7 +298,7 @@ export const ManagePublicTemplateDialog = ({
</TableCell> </TableCell>
<TableCell className="text-muted-foreground text-sm"> <TableCell className="text-muted-foreground text-sm">
<LocaleDate date={row.createdAt} /> {i18n.date(row.createdAt)}
</TableCell> </TableCell>
<TableCell> <TableCell>

View File

@ -5,7 +5,6 @@ import { NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt'; import { getToken } from 'next-auth/jwt';
import { TEAM_URL_ROOT_REGEX } from '@documenso/lib/constants/teams'; import { TEAM_URL_ROOT_REGEX } from '@documenso/lib/constants/teams';
import { extractSupportedLanguage } from '@documenso/lib/utils/i18n';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
async function middleware(req: NextRequest): Promise<NextResponse> { async function middleware(req: NextRequest): Promise<NextResponse> {
@ -96,12 +95,7 @@ async function middleware(req: NextRequest): Promise<NextResponse> {
export default async function middlewareWrapper(req: NextRequest) { export default async function middlewareWrapper(req: NextRequest) {
const response = await middleware(req); const response = await middleware(req);
const lang = extractSupportedLanguage({ // Can place anything that needs to be set on the response here.
headers: req.headers,
cookies: cookies(),
});
response.cookies.set('i18n', lang);
return response; return response;
} }

View File

@ -74,6 +74,7 @@ docker run -d \
-e NEXT_PRIVATE_ENCRYPTION_KEY="<your-next-private-encryption-key>" -e NEXT_PRIVATE_ENCRYPTION_KEY="<your-next-private-encryption-key>"
-e NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="<your-next-private-encryption-secondary-key>" -e NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="<your-next-private-encryption-secondary-key>"
-e NEXT_PUBLIC_WEBAPP_URL="<your-next-public-webapp-url>" -e NEXT_PUBLIC_WEBAPP_URL="<your-next-public-webapp-url>"
-e NEXT_PRIVATE_INTERNAL_WEBAPP_URL="http://localhost:3000"
-e NEXT_PRIVATE_DATABASE_URL="<your-next-private-database-url>" -e NEXT_PRIVATE_DATABASE_URL="<your-next-private-database-url>"
-e NEXT_PRIVATE_DIRECT_DATABASE_URL="<your-next-private-database-url>" -e NEXT_PRIVATE_DIRECT_DATABASE_URL="<your-next-private-database-url>"
-e NEXT_PRIVATE_SMTP_TRANSPORT="<your-next-private-smtp-transport>" -e NEXT_PRIVATE_SMTP_TRANSPORT="<your-next-private-smtp-transport>"

View File

@ -29,6 +29,7 @@ services:
- NEXT_PRIVATE_GOOGLE_CLIENT_ID=${NEXT_PRIVATE_GOOGLE_CLIENT_ID} - NEXT_PRIVATE_GOOGLE_CLIENT_ID=${NEXT_PRIVATE_GOOGLE_CLIENT_ID}
- NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=${NEXT_PRIVATE_GOOGLE_CLIENT_SECRET} - NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=${NEXT_PRIVATE_GOOGLE_CLIENT_SECRET}
- NEXT_PUBLIC_WEBAPP_URL=${NEXT_PUBLIC_WEBAPP_URL:?err} - NEXT_PUBLIC_WEBAPP_URL=${NEXT_PUBLIC_WEBAPP_URL:?err}
- NEXT_PRIVATE_INTERNAL_WEBAPP_URL=${NEXT_PRIVATE_INTERNAL_WEBAPP_URL:-http://localhost:$PORT}
- NEXT_PUBLIC_MARKETING_URL=${NEXT_PUBLIC_MARKETING_URL:-https://documenso.com} - NEXT_PUBLIC_MARKETING_URL=${NEXT_PUBLIC_MARKETING_URL:-https://documenso.com}
- NEXT_PRIVATE_DATABASE_URL=${NEXT_PRIVATE_DATABASE_URL:?err} - NEXT_PRIVATE_DATABASE_URL=${NEXT_PRIVATE_DATABASE_URL:?err}
- NEXT_PRIVATE_DIRECT_DATABASE_URL=${NEXT_PRIVATE_DIRECT_DATABASE_URL:-${NEXT_PRIVATE_DATABASE_URL}} - NEXT_PRIVATE_DIRECT_DATABASE_URL=${NEXT_PRIVATE_DIRECT_DATABASE_URL:-${NEXT_PRIVATE_DATABASE_URL}}

8
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "@documenso/root", "name": "@documenso/root",
"version": "1.7.0-rc.4", "version": "1.7.1-rc.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@documenso/root", "name": "@documenso/root",
"version": "1.7.0-rc.4", "version": "1.7.1-rc.0",
"workspaces": [ "workspaces": [
"apps/*", "apps/*",
"packages/*" "packages/*"
@ -80,7 +80,7 @@
}, },
"apps/marketing": { "apps/marketing": {
"name": "@documenso/marketing", "name": "@documenso/marketing",
"version": "1.7.0-rc.4", "version": "1.7.1-rc.0",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@documenso/assets": "*", "@documenso/assets": "*",
@ -424,7 +424,7 @@
}, },
"apps/web": { "apps/web": {
"name": "@documenso/web", "name": "@documenso/web",
"version": "1.7.0-rc.4", "version": "1.7.1-rc.0",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@documenso/api": "*", "@documenso/api": "*",

View File

@ -1,6 +1,6 @@
{ {
"private": true, "private": true,
"version": "1.7.0-rc.4", "version": "1.7.1-rc.0",
"scripts": { "scripts": {
"build": "turbo run build", "build": "turbo run build",
"build:web": "turbo run build --filter=@documenso/web", "build:web": "turbo run build --filter=@documenso/web",

View File

@ -16,6 +16,7 @@ import { getDocumentById } from '@documenso/lib/server-only/document/get-documen
import { resendDocument } from '@documenso/lib/server-only/document/resend-document'; import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
import { sendDocument } from '@documenso/lib/server-only/document/send-document'; import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { updateDocument } from '@documenso/lib/server-only/document/update-document'; import { updateDocument } from '@documenso/lib/server-only/document/update-document';
import { updateDocumentSettings } from '@documenso/lib/server-only/document/update-document-settings';
import { deleteField } from '@documenso/lib/server-only/field/delete-field'; import { deleteField } from '@documenso/lib/server-only/field/delete-field';
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id'; import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
import { updateField } from '@documenso/lib/server-only/field/update-field'; import { updateField } from '@documenso/lib/server-only/field/update-field';
@ -295,6 +296,16 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
requestMetadata: extractNextApiRequestMetadata(args.req), requestMetadata: extractNextApiRequestMetadata(args.req),
}); });
if (body.authOptions) {
await updateDocumentSettings({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: body.authOptions,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
}
const recipients = await setRecipientsForDocument({ const recipients = await setRecipientsForDocument({
userId: user.id, userId: user.id,
teamId: team?.id, teamId: team?.id,
@ -465,6 +476,16 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
}); });
} }
if (body.authOptions) {
await updateDocumentSettings({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: body.authOptions,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
}
return { return {
status: 200, status: 200,
body: { body: {
@ -547,6 +568,16 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
}); });
} }
if (body.authOptions) {
await updateDocumentSettings({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: body.authOptions,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
}
return { return {
status: 200, status: 200,
body: { body: {
@ -682,7 +713,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
createRecipient: authenticatedMiddleware(async (args, user, team) => { createRecipient: authenticatedMiddleware(async (args, user, team) => {
const { id: documentId } = args.params; const { id: documentId } = args.params;
const { name, email, role } = args.body; const { name, email, role, authOptions } = args.body;
const document = await getDocumentById({ const document = await getDocumentById({
id: Number(documentId), id: Number(documentId),
@ -736,6 +767,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
email, email,
name, name,
role, role,
actionAuth: authOptions?.actionAuth ?? null,
}, },
], ],
requestMetadata: extractNextApiRequestMetadata(args.req), requestMetadata: extractNextApiRequestMetadata(args.req),
@ -767,7 +799,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
updateRecipient: authenticatedMiddleware(async (args, user, team) => { updateRecipient: authenticatedMiddleware(async (args, user, team) => {
const { id: documentId, recipientId } = args.params; const { id: documentId, recipientId } = args.params;
const { name, email, role } = args.body; const { name, email, role, authOptions } = args.body;
const document = await getDocumentById({ const document = await getDocumentById({
id: Number(documentId), id: Number(documentId),
@ -801,6 +833,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
email, email,
name, name,
role, role,
actionAuth: authOptions?.actionAuth,
requestMetadata: extractNextApiRequestMetadata(args.req), requestMetadata: extractNextApiRequestMetadata(args.req),
}).catch(() => null); }).catch(() => null);

View File

@ -5,6 +5,11 @@ import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/const
import '@documenso/lib/constants/time-zones'; import '@documenso/lib/constants/time-zones';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones'; import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { ZUrlSchema } from '@documenso/lib/schemas/common'; import { ZUrlSchema } from '@documenso/lib/schemas/common';
import {
ZDocumentAccessAuthTypesSchema,
ZDocumentActionAuthTypesSchema,
ZRecipientActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta'; import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { import {
DocumentDataType, DocumentDataType,
@ -120,6 +125,12 @@ export const ZCreateDocumentMutationSchema = z.object({
redirectUrl: z.string(), redirectUrl: z.string(),
}) })
.partial(), .partial(),
authOptions: z
.object({
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
})
.optional(),
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(), formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
}); });
@ -166,6 +177,12 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
}) })
.partial() .partial()
.optional(), .optional(),
authOptions: z
.object({
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
})
.optional(),
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(), formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
}); });
@ -223,6 +240,12 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
}) })
.partial() .partial()
.optional(), .optional(),
authOptions: z
.object({
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
})
.optional(),
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(), formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
}); });
@ -254,6 +277,11 @@ export const ZCreateRecipientMutationSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
email: z.string().email().min(1), email: z.string().email().min(1),
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER), role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
authOptions: z
.object({
actionAuth: ZRecipientActionAuthTypesSchema.optional(),
})
.optional(),
}); });
/** /**

View File

@ -5,19 +5,24 @@ import { useState } from 'react';
import { type Messages, setupI18n } from '@lingui/core'; import { type Messages, setupI18n } from '@lingui/core';
import { I18nProvider } from '@lingui/react'; import { I18nProvider } from '@lingui/react';
import type { I18nLocaleData } from '../../constants/i18n';
export function I18nClientProvider({ export function I18nClientProvider({
children, children,
initialLocale, initialLocaleData,
initialMessages, initialMessages,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
initialLocale: string; initialLocaleData: I18nLocaleData;
initialMessages: Messages; initialMessages: Messages;
}) { }) {
const { lang, locales } = initialLocaleData;
const [i18n] = useState(() => { const [i18n] = useState(() => {
return setupI18n({ return setupI18n({
locale: initialLocale, locale: lang,
messages: { [initialLocale]: initialMessages }, locales: locales,
messages: { [lang]: initialMessages },
}); });
}); });

View File

@ -1,26 +1,26 @@
import 'server-only'; import 'server-only';
import { cookies } from 'next/headers'; import { cookies, headers } from 'next/headers';
import type { I18n, Messages } from '@lingui/core'; import type { I18n, Messages } from '@lingui/core';
import { setupI18n } from '@lingui/core'; import { setupI18n } from '@lingui/core';
import { setI18n } from '@lingui/react/server'; import { setI18n } from '@lingui/react/server';
import { IS_APP_WEB, IS_APP_WEB_I18N_ENABLED } from '../../constants/app'; import { IS_APP_WEB } from '../../constants/app';
import { SUPPORTED_LANGUAGE_CODES } from '../../constants/i18n'; import { SUPPORTED_LANGUAGE_CODES } from '../../constants/i18n';
import { extractSupportedLanguage } from '../../utils/i18n'; import { extractLocaleData } from '../../utils/i18n';
type SupportedLocales = (typeof SUPPORTED_LANGUAGE_CODES)[number]; type SupportedLanguages = (typeof SUPPORTED_LANGUAGE_CODES)[number];
async function loadCatalog(locale: SupportedLocales): Promise<{ async function loadCatalog(lang: SupportedLanguages): Promise<{
[k: string]: Messages; [k: string]: Messages;
}> { }> {
const { messages } = await import( const { messages } = await import(
`../../translations/${locale}/${IS_APP_WEB ? 'web' : 'marketing'}.js` `../../translations/${lang}/${IS_APP_WEB ? 'web' : 'marketing'}.js`
); );
return { return {
[locale]: messages, [lang]: messages,
}; };
} }
@ -31,18 +31,18 @@ export const allMessages = catalogs.reduce((acc, oneCatalog) => {
return { ...acc, ...oneCatalog }; return { ...acc, ...oneCatalog };
}, {}); }, {});
type AllI18nInstances = { [K in SupportedLocales]: I18n }; type AllI18nInstances = { [K in SupportedLanguages]: I18n };
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
export const allI18nInstances = SUPPORTED_LANGUAGE_CODES.reduce((acc, locale) => { export const allI18nInstances = SUPPORTED_LANGUAGE_CODES.reduce((acc, lang) => {
const messages = allMessages[locale] ?? {}; const messages = allMessages[lang] ?? {};
const i18n = setupI18n({ const i18n = setupI18n({
locale, locale: lang,
messages: { [locale]: messages }, messages: { [lang]: messages },
}); });
return { ...acc, [locale]: i18n }; return { ...acc, [lang]: i18n };
}, {}) as AllI18nInstances; }, {}) as AllI18nInstances;
/** /**
@ -50,24 +50,23 @@ export const allI18nInstances = SUPPORTED_LANGUAGE_CODES.reduce((acc, locale) =>
* *
* https://lingui.dev/tutorials/react-rsc#pages-layouts-and-lingui * https://lingui.dev/tutorials/react-rsc#pages-layouts-and-lingui
*/ */
export const setupI18nSSR = (overrideLang?: SupportedLocales) => { export const setupI18nSSR = () => {
let lang = const { lang, locales } = extractLocaleData({
overrideLang || cookies: cookies(),
extractSupportedLanguage({ headers: headers(),
cookies: cookies(), });
});
// Override web app to be English.
if (!IS_APP_WEB_I18N_ENABLED && IS_APP_WEB) {
lang = 'en';
}
// Get and set a ready-made i18n instance for the given language. // Get and set a ready-made i18n instance for the given language.
const i18n = allI18nInstances[lang]; const i18n = allI18nInstances[lang];
// Reactivate the i18n instance with the locale for date and number formatting.
i18n.activate(lang, locales);
setI18n(i18n); setI18n(i18n);
return { return {
lang, lang,
locales,
i18n, i18n,
}; };
}; };

View File

@ -1,37 +0,0 @@
'use client';
import { createContext, useContext } from 'react';
export type LocaleContextValue = {
locale: string;
};
export const LocaleContext = createContext<LocaleContextValue | null>(null);
export const useLocale = () => {
const context = useContext(LocaleContext);
if (!context) {
throw new Error('useLocale must be used within a LocaleProvider');
}
return context;
};
export function LocaleProvider({
children,
locale,
}: {
children: React.ReactNode;
locale: string;
}) {
return (
<LocaleContext.Provider
value={{
locale: locale,
}}
>
{children}
</LocaleContext.Provider>
);
}

View File

@ -1,15 +1,16 @@
import { env } from 'next-runtime-env'; import { env } from 'next-runtime-env';
export const IS_APP_WEB_I18N_ENABLED = false;
export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT = export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT =
Number(process.env.NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT) || 50; Number(process.env.NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT) || 50;
export const NEXT_PUBLIC_WEBAPP_URL = () => env('NEXT_PUBLIC_WEBAPP_URL'); export const NEXT_PUBLIC_WEBAPP_URL = () => env('NEXT_PUBLIC_WEBAPP_URL');
export const NEXT_PUBLIC_MARKETING_URL = () => env('NEXT_PUBLIC_MARKETING_URL'); export const NEXT_PUBLIC_MARKETING_URL = () => env('NEXT_PUBLIC_MARKETING_URL');
export const NEXT_PRIVATE_INTERNAL_WEBAPP_URL = process.env.NEXT_PRIVATE_INTERNAL_WEBAPP_URL ?? NEXT_PUBLIC_WEBAPP_URL();
export const IS_APP_MARKETING = process.env.NEXT_PUBLIC_PROJECT === 'marketing'; export const IS_APP_MARKETING = process.env.NEXT_PUBLIC_PROJECT === 'marketing';
export const IS_APP_WEB = process.env.NEXT_PUBLIC_PROJECT === 'web'; export const IS_APP_WEB = process.env.NEXT_PUBLIC_PROJECT === 'web';
export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED') === 'true'; export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED') === 'true';
export const IS_APP_WEB_I18N_ENABLED = true;
export const APP_FOLDER = () => (IS_APP_MARKETING ? 'marketing' : 'web'); export const APP_FOLDER = () => (IS_APP_MARKETING ? 'marketing' : 'web');

View File

@ -6,9 +6,22 @@ export const ZSupportedLanguageCodeSchema = z.enum(SUPPORTED_LANGUAGE_CODES).cat
export type SupportedLanguageCodes = (typeof SUPPORTED_LANGUAGE_CODES)[number]; export type SupportedLanguageCodes = (typeof SUPPORTED_LANGUAGE_CODES)[number];
export type I18nLocaleData = {
/**
* The supported language extracted from the locale.
*/
lang: SupportedLanguageCodes;
/**
* The preferred locales.
*/
locales: string[];
};
export const APP_I18N_OPTIONS = { export const APP_I18N_OPTIONS = {
supportedLangs: SUPPORTED_LANGUAGE_CODES, supportedLangs: SUPPORTED_LANGUAGE_CODES,
sourceLang: 'en', sourceLang: 'en',
defaultLocale: 'en-US',
} as const; } as const;
type SupportedLanguage = { type SupportedLanguage = {

View File

@ -6,7 +6,7 @@ import { json } from 'micro';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { BackgroundJobStatus, Prisma } from '@documenso/prisma/client'; import { BackgroundJobStatus, Prisma } from '@documenso/prisma/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; import { NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
import { sign } from '../../server-only/crypto/sign'; import { sign } from '../../server-only/crypto/sign';
import { verify } from '../../server-only/crypto/verify'; import { verify } from '../../server-only/crypto/verify';
import { import {
@ -229,7 +229,7 @@ export class LocalJobProvider extends BaseJobProvider {
}) { }) {
const { jobId, jobDefinitionId, data, isRetry } = options; const { jobId, jobDefinitionId, data, isRetry } = options;
const endpoint = `${NEXT_PUBLIC_WEBAPP_URL()}/api/jobs/${jobDefinitionId}/${jobId}`; const endpoint = `${NEXT_PRIVATE_INTERNAL_WEBAPP_URL}/api/jobs/${jobDefinitionId}/${jobId}`;
const signature = sign(data); const signature = sign(data);
const headers: Record<string, string> = { const headers: Record<string, string> = {

View File

@ -5,7 +5,7 @@ import { getToken } from 'next-auth/jwt';
import { LOCAL_FEATURE_FLAGS } from '@documenso/lib/constants/feature-flags'; import { LOCAL_FEATURE_FLAGS } from '@documenso/lib/constants/feature-flags';
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client'; import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
import { NEXT_PUBLIC_MARKETING_URL, NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; import { NEXT_PUBLIC_MARKETING_URL, NEXT_PUBLIC_WEBAPP_URL, NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
import { extractDistinctUserId, mapJwtToFlagProperties } from './get'; import { extractDistinctUserId, mapJwtToFlagProperties } from './get';
/** /**
@ -46,6 +46,10 @@ export default async function handlerFeatureFlagAll(req: Request) {
if (origin.startsWith(NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3001')) { if (origin.startsWith(NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3001')) {
res.headers.set('Access-Control-Allow-Origin', origin); res.headers.set('Access-Control-Allow-Origin', origin);
} }
if (origin.startsWith(NEXT_PRIVATE_INTERNAL_WEBAPP_URL ?? 'http://localhost:3000')) {
res.headers.set('Access-Control-Allow-Origin', origin);
}
} }
return res; return res;

View File

@ -7,7 +7,7 @@ import { getToken } from 'next-auth/jwt';
import { LOCAL_FEATURE_FLAGS, extractPostHogConfig } from '@documenso/lib/constants/feature-flags'; import { LOCAL_FEATURE_FLAGS, extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client'; import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
import { NEXT_PUBLIC_MARKETING_URL, NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; import { NEXT_PUBLIC_MARKETING_URL, NEXT_PUBLIC_WEBAPP_URL, NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
/** /**
* Evaluate a single feature flag based on the current user if possible. * Evaluate a single feature flag based on the current user if possible.
@ -67,6 +67,10 @@ export default async function handleFeatureFlagGet(req: Request) {
if (origin.startsWith(NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3001')) { if (origin.startsWith(NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3001')) {
res.headers.set('Access-Control-Allow-Origin', origin); res.headers.set('Access-Control-Allow-Origin', origin);
} }
if (origin.startsWith(NEXT_PRIVATE_INTERNAL_WEBAPP_URL ?? 'http://localhost:3000')) {
res.headers.set('Access-Control-Allow-Origin', origin);
}
} }
return res; return res;

View File

@ -107,7 +107,10 @@ export const setFieldsForTemplate = async ({
} }
} }
if (field.type === FieldType.CHECKBOX && field.fieldMeta) { if (field.type === FieldType.CHECKBOX) {
if (!field.fieldMeta) {
throw new Error('Checkbox field is missing required metadata');
}
const checkboxFieldParsedMeta = ZCheckboxFieldMeta.parse(field.fieldMeta); const checkboxFieldParsedMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
const errors = validateCheckboxField( const errors = validateCheckboxField(
checkboxFieldParsedMeta?.values?.map((item) => item.value) ?? [], checkboxFieldParsedMeta?.values?.map((item) => item.value) ?? [],
@ -118,7 +121,10 @@ export const setFieldsForTemplate = async ({
} }
} }
if (field.type === FieldType.RADIO && field.fieldMeta) { if (field.type === FieldType.RADIO) {
if (!field.fieldMeta) {
throw new Error('Radio field is missing required metadata');
}
const radioFieldParsedMeta = ZRadioFieldMeta.parse(field.fieldMeta); const radioFieldParsedMeta = ZRadioFieldMeta.parse(field.fieldMeta);
const checkedRadioFieldValue = radioFieldParsedMeta.values?.find( const checkedRadioFieldValue = radioFieldParsedMeta.values?.find(
(option) => option.checked, (option) => option.checked,
@ -129,7 +135,10 @@ export const setFieldsForTemplate = async ({
} }
} }
if (field.type === FieldType.DROPDOWN && field.fieldMeta) { if (field.type === FieldType.DROPDOWN) {
if (!field.fieldMeta) {
throw new Error('Dropdown field is missing required metadata');
}
const dropdownFieldParsedMeta = ZDropdownFieldMeta.parse(field.fieldMeta); const dropdownFieldParsedMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
const errors = validateDropdownField(undefined, dropdownFieldParsedMeta); const errors = validateDropdownField(undefined, dropdownFieldParsedMeta);
if (errors.length > 0) { if (errors.length > 0) {

View File

@ -1,11 +0,0 @@
import { headers } from 'next/headers';
export const getLocale = () => {
const headerItems = headers();
const locales = headerItems.get('accept-language') ?? 'en-US';
const [locale] = locales.split(',');
return locale;
};

View File

@ -4,5 +4,5 @@ import { cookies } from 'next/headers';
// eslint-disable-next-line @typescript-eslint/require-await // eslint-disable-next-line @typescript-eslint/require-await
export const switchI18NLanguage = async (lang: string) => { export const switchI18NLanguage = async (lang: string) => {
cookies().set('i18n', lang); cookies().set('language', lang);
}; };

View File

@ -1,9 +1,16 @@
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import type { RecipientRole, Team } from '@documenso/prisma/client'; import type { RecipientRole, Team } from '@documenso/prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs'; import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import {
type TRecipientActionAuthTypes,
ZRecipientAuthOptionsSchema,
} from '../../types/document-auth';
import type { RequestMetadata } from '../../universal/extract-request-metadata'; import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { createDocumentAuditLogData, diffRecipientChanges } from '../../utils/document-audit-logs'; import { createDocumentAuditLogData, diffRecipientChanges } from '../../utils/document-audit-logs';
import { createRecipientAuthOptions } from '../../utils/document-auth';
export type UpdateRecipientOptions = { export type UpdateRecipientOptions = {
documentId: number; documentId: number;
@ -11,6 +18,7 @@ export type UpdateRecipientOptions = {
email?: string; email?: string;
name?: string; name?: string;
role?: RecipientRole; role?: RecipientRole;
actionAuth?: TRecipientActionAuthTypes | null;
userId: number; userId: number;
teamId?: number; teamId?: number;
requestMetadata?: RequestMetadata; requestMetadata?: RequestMetadata;
@ -22,6 +30,7 @@ export const updateRecipient = async ({
email, email,
name, name,
role, role,
actionAuth,
userId, userId,
teamId, teamId,
requestMetadata, requestMetadata,
@ -48,6 +57,9 @@ export const updateRecipient = async ({
}), }),
}, },
}, },
include: {
Document: true,
},
}); });
let team: Team | null = null; let team: Team | null = null;
@ -75,6 +87,22 @@ export const updateRecipient = async ({
throw new Error('Recipient not found'); throw new Error('Recipient not found');
} }
if (actionAuth) {
const isDocumentEnterprise = await isUserEnterprise({
userId,
teamId,
});
if (!isDocumentEnterprise) {
throw new AppError(
AppErrorCode.UNAUTHORIZED,
'You do not have permission to set the action auth',
);
}
}
const recipientAuthOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
const updatedRecipient = await prisma.$transaction(async (tx) => { const updatedRecipient = await prisma.$transaction(async (tx) => {
const persisted = await prisma.recipient.update({ const persisted = await prisma.recipient.update({
where: { where: {
@ -84,6 +112,10 @@ export const updateRecipient = async ({
email: email?.toLowerCase() ?? recipient.email, email: email?.toLowerCase() ?? recipient.email,
name: name ?? recipient.name, name: name ?? recipient.name,
role: role ?? recipient.role, role: role ?? recipient.role,
authOptions: createRecipientAuthOptions({
accessAuth: recipientAuthOptions.accessAuth,
actionAuth: actionAuth ?? null,
}),
}, },
}); });

View File

@ -1,6 +1,6 @@
import type { WebhookTriggerEvents } from '@documenso/prisma/client'; import type { WebhookTriggerEvents } from '@documenso/prisma/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app'; import { NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../../constants/app';
import { sign } from '../../crypto/sign'; import { sign } from '../../crypto/sign';
import { getAllWebhooksByEventTrigger } from '../get-all-webhooks-by-event-trigger'; import { getAllWebhooksByEventTrigger } from '../get-all-webhooks-by-event-trigger';
@ -29,7 +29,7 @@ export const triggerWebhook = async ({ event, data, userId, teamId }: TriggerWeb
const signature = sign(body); const signature = sign(body);
await Promise.race([ await Promise.race([
fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/api/webhook/trigger`, { fetch(`${NEXT_PRIVATE_INTERNAL_WEBAPP_URL}/api/webhook/trigger`, {
method: 'POST', method: 'POST',
headers: { headers: {
'content-type': 'application/json', 'content-type': 'application/json',

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: de\n" "Language: de\n"
"Project-Id-Version: documenso-app\n" "Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-08-27 16:03\n" "PO-Revision-Date: 2024-09-05 06:04\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: German\n" "Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@ -116,7 +116,7 @@ msgid "Advanced Options"
msgstr "Erweiterte Optionen" msgstr "Erweiterte Optionen"
#: packages/ui/primitives/document-flow/add-fields.tsx:510 #: packages/ui/primitives/document-flow/add-fields.tsx:510
#: packages/ui/primitives/template-flow/add-template-fields.tsx:370 #: packages/ui/primitives/template-flow/add-template-fields.tsx:402
msgid "Advanced settings" msgid "Advanced settings"
msgstr "Erweiterte Einstellungen" msgstr "Erweiterte Einstellungen"
@ -140,7 +140,15 @@ msgstr "Genehmiger"
msgid "Approving" msgid "Approving"
msgstr "Genehmigung" msgstr "Genehmigung"
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:297 #: packages/ui/primitives/signature-pad/signature-pad.tsx:276
msgid "Black"
msgstr ""
#: packages/ui/primitives/signature-pad/signature-pad.tsx:290
msgid "Blue"
msgstr ""
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:287
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:58 #: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:58
msgid "Cancel" msgid "Cancel"
msgstr "Abbrechen" msgstr "Abbrechen"
@ -167,7 +175,7 @@ msgid "Character Limit"
msgstr "Zeichenbeschränkung" msgstr "Zeichenbeschränkung"
#: packages/ui/primitives/document-flow/add-fields.tsx:932 #: packages/ui/primitives/document-flow/add-fields.tsx:932
#: packages/ui/primitives/template-flow/add-template-fields.tsx:756 #: packages/ui/primitives/template-flow/add-template-fields.tsx:788
msgid "Checkbox" msgid "Checkbox"
msgstr "Checkbox" msgstr "Checkbox"
@ -179,7 +187,7 @@ msgstr "Checkbox-Werte"
msgid "Clear filters" msgid "Clear filters"
msgstr "Filter löschen" msgstr "Filter löschen"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:256 #: packages/ui/primitives/signature-pad/signature-pad.tsx:310
msgid "Clear Signature" msgid "Clear Signature"
msgstr "Unterschrift löschen" msgstr "Unterschrift löschen"
@ -196,7 +204,7 @@ msgid "Configure Direct Recipient"
msgstr "Direkten Empfänger konfigurieren" msgstr "Direkten Empfänger konfigurieren"
#: packages/ui/primitives/document-flow/add-fields.tsx:511 #: packages/ui/primitives/document-flow/add-fields.tsx:511
#: packages/ui/primitives/template-flow/add-template-fields.tsx:371 #: packages/ui/primitives/template-flow/add-template-fields.tsx:403
msgid "Configure the {0} field" msgid "Configure the {0} field"
msgstr "Konfigurieren Sie das Feld {0}" msgstr "Konfigurieren Sie das Feld {0}"
@ -213,7 +221,7 @@ msgid "Custom Text"
msgstr "Benutzerdefinierter Text" msgstr "Benutzerdefinierter Text"
#: packages/ui/primitives/document-flow/add-fields.tsx:828 #: packages/ui/primitives/document-flow/add-fields.tsx:828
#: packages/ui/primitives/template-flow/add-template-fields.tsx:652 #: packages/ui/primitives/template-flow/add-template-fields.tsx:684
msgid "Date" msgid "Date"
msgstr "Datum" msgstr "Datum"
@ -245,7 +253,7 @@ msgid "Drag & drop your PDF here."
msgstr "Ziehen Sie Ihr PDF hierher." msgstr "Ziehen Sie Ihr PDF hierher."
#: packages/ui/primitives/document-flow/add-fields.tsx:958 #: packages/ui/primitives/document-flow/add-fields.tsx:958
#: packages/ui/primitives/template-flow/add-template-fields.tsx:782 #: packages/ui/primitives/template-flow/add-template-fields.tsx:814
msgid "Dropdown" msgid "Dropdown"
msgstr "Dropdown" msgstr "Dropdown"
@ -257,7 +265,7 @@ msgstr "Dropdown-Optionen"
#: packages/ui/primitives/document-flow/add-signature.tsx:272 #: packages/ui/primitives/document-flow/add-signature.tsx:272
#: packages/ui/primitives/document-flow/add-signers.tsx:232 #: packages/ui/primitives/document-flow/add-signers.tsx:232
#: packages/ui/primitives/document-flow/add-signers.tsx:239 #: packages/ui/primitives/document-flow/add-signers.tsx:239
#: packages/ui/primitives/template-flow/add-template-fields.tsx:600 #: packages/ui/primitives/template-flow/add-template-fields.tsx:632
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:210 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:210
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:217 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:217
msgid "Email" msgid "Email"
@ -275,7 +283,7 @@ msgstr "Direktlink-Signierung aktivieren"
msgid "Enter password" msgid "Enter password"
msgstr "Passwort eingeben" msgstr "Passwort eingeben"
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:226 #: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:216
msgid "Error" msgid "Error"
msgstr "Fehler" msgstr "Fehler"
@ -284,7 +292,7 @@ msgstr "Fehler"
msgid "External ID" msgid "External ID"
msgstr "Externe ID" msgstr "Externe ID"
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:227 #: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:217
msgid "Failed to save settings." msgid "Failed to save settings."
msgstr "Einstellungen konnten nicht gespeichert werden." msgstr "Einstellungen konnten nicht gespeichert werden."
@ -312,6 +320,10 @@ msgstr "Globale Empfängerauthentifizierung"
msgid "Go Back" msgid "Go Back"
msgstr "Zurück" msgstr "Zurück"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:297
msgid "Green"
msgstr ""
#: packages/lib/constants/recipient-roles.ts:72 #: packages/lib/constants/recipient-roles.ts:72
msgid "I am a signer of this document" msgid "I am a signer of this document"
msgstr "Ich bin ein Unterzeichner dieses Dokuments" msgstr "Ich bin ein Unterzeichner dieses Dokuments"
@ -367,7 +379,7 @@ msgstr "Min"
#: packages/ui/primitives/document-flow/add-fields.tsx:802 #: packages/ui/primitives/document-flow/add-fields.tsx:802
#: packages/ui/primitives/document-flow/add-signature.tsx:298 #: packages/ui/primitives/document-flow/add-signature.tsx:298
#: packages/ui/primitives/document-flow/add-signers.tsx:265 #: packages/ui/primitives/document-flow/add-signers.tsx:265
#: packages/ui/primitives/template-flow/add-template-fields.tsx:626 #: packages/ui/primitives/template-flow/add-template-fields.tsx:658
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:245 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:245
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:251 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:251
msgid "Name" msgid "Name"
@ -386,12 +398,12 @@ msgid "Needs to view"
msgstr "Muss sehen" msgstr "Muss sehen"
#: packages/ui/primitives/document-flow/add-fields.tsx:613 #: packages/ui/primitives/document-flow/add-fields.tsx:613
#: packages/ui/primitives/template-flow/add-template-fields.tsx:465 #: packages/ui/primitives/template-flow/add-template-fields.tsx:497
msgid "No recipient matching this description was found." msgid "No recipient matching this description was found."
msgstr "Kein passender Empfänger mit dieser Beschreibung gefunden." msgstr "Kein passender Empfänger mit dieser Beschreibung gefunden."
#: packages/ui/primitives/document-flow/add-fields.tsx:629 #: packages/ui/primitives/document-flow/add-fields.tsx:629
#: packages/ui/primitives/template-flow/add-template-fields.tsx:481 #: packages/ui/primitives/template-flow/add-template-fields.tsx:513
msgid "No recipients with this role" msgid "No recipients with this role"
msgstr "Keine Empfänger mit dieser Rolle" msgstr "Keine Empfänger mit dieser Rolle"
@ -416,7 +428,7 @@ msgid "No value found."
msgstr "Kein Wert gefunden." msgstr "Kein Wert gefunden."
#: packages/ui/primitives/document-flow/add-fields.tsx:880 #: packages/ui/primitives/document-flow/add-fields.tsx:880
#: packages/ui/primitives/template-flow/add-template-fields.tsx:704 #: packages/ui/primitives/template-flow/add-template-fields.tsx:736
msgid "Number" msgid "Number"
msgstr "Nummer" msgstr "Nummer"
@ -451,7 +463,7 @@ msgid "Placeholder"
msgstr "Platzhalter" msgstr "Platzhalter"
#: packages/ui/primitives/document-flow/add-fields.tsx:906 #: packages/ui/primitives/document-flow/add-fields.tsx:906
#: packages/ui/primitives/template-flow/add-template-fields.tsx:730 #: packages/ui/primitives/template-flow/add-template-fields.tsx:762
msgid "Radio" msgid "Radio"
msgstr "Radio" msgstr "Radio"
@ -477,6 +489,10 @@ msgstr "Erhält Kopie"
msgid "Recipient action authentication" msgid "Recipient action authentication"
msgstr "Empfängeraktion Authentifizierung" msgstr "Empfängeraktion Authentifizierung"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:283
msgid "Red"
msgstr ""
#: packages/ui/primitives/document-flow/add-settings.tsx:298 #: packages/ui/primitives/document-flow/add-settings.tsx:298
#: packages/ui/primitives/template-flow/add-template-settings.tsx:332 #: packages/ui/primitives/template-flow/add-template-settings.tsx:332
msgid "Redirect URL" msgid "Redirect URL"
@ -498,14 +514,18 @@ msgstr "Pflichtfeld"
msgid "Rows per page" msgid "Rows per page"
msgstr "Zeilen pro Seite" msgstr "Zeilen pro Seite"
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:296 #: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:286
msgid "Save" msgid "Save"
msgstr "Speichern" msgstr "Speichern"
#: packages/ui/primitives/template-flow/add-template-fields.tsx:798 #: packages/ui/primitives/template-flow/add-template-fields.tsx:848
msgid "Save Template" msgid "Save Template"
msgstr "Vorlage speichern" msgstr "Vorlage speichern"
#: packages/ui/components/common/language-switcher-dialog.tsx:34
msgid "Search languages..."
msgstr "Sprachen suchen..."
#: packages/ui/primitives/document-flow/field-items-advanced-settings/dropdown-field.tsx:105 #: packages/ui/primitives/document-flow/field-items-advanced-settings/dropdown-field.tsx:105
msgid "Select" msgid "Select"
msgstr "Auswählen" msgstr "Auswählen"
@ -552,7 +572,7 @@ msgstr "Unterschreiben"
#: packages/ui/primitives/document-flow/add-fields.tsx:724 #: packages/ui/primitives/document-flow/add-fields.tsx:724
#: packages/ui/primitives/document-flow/add-signature.tsx:323 #: packages/ui/primitives/document-flow/add-signature.tsx:323
#: packages/ui/primitives/document-flow/field-icon.tsx:52 #: packages/ui/primitives/document-flow/field-icon.tsx:52
#: packages/ui/primitives/template-flow/add-template-fields.tsx:548 #: packages/ui/primitives/template-flow/add-template-fields.tsx:580
msgid "Signature" msgid "Signature"
msgstr "Unterschrift" msgstr "Unterschrift"
@ -598,7 +618,7 @@ msgid "Template title"
msgstr "Vorlagentitel" msgstr "Vorlagentitel"
#: packages/ui/primitives/document-flow/add-fields.tsx:854 #: packages/ui/primitives/document-flow/add-fields.tsx:854
#: packages/ui/primitives/template-flow/add-template-fields.tsx:678 #: packages/ui/primitives/template-flow/add-template-fields.tsx:710
msgid "Text" msgid "Text"
msgstr "Text" msgstr "Text"
@ -688,6 +708,7 @@ msgid "Title"
msgstr "Titel" msgstr "Titel"
#: packages/ui/primitives/document-flow/add-fields.tsx:971 #: packages/ui/primitives/document-flow/add-fields.tsx:971
#: packages/ui/primitives/template-flow/add-template-fields.tsx:828
msgid "To proceed further, please set at least one value for the {0} field." msgid "To proceed further, please set at least one value for the {0} field."
msgstr "Um fortzufahren, legen Sie bitte mindestens einen Wert für das Feld {0} fest." msgstr "Um fortzufahren, legen Sie bitte mindestens einen Wert für das Feld {0} fest."
@ -733,6 +754,10 @@ msgstr "Viewer"
msgid "Viewing" msgid "Viewing"
msgstr "Viewing" msgstr "Viewing"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:280
#~ msgid "White"
#~ msgstr ""
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:44 #: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:44
msgid "You are about to send this document to the recipients. Are you sure you want to continue?" msgid "You are about to send this document to the recipients. Are you sure you want to continue?"
msgstr "Sie sind dabei, dieses Dokument an die Empfänger zu senden. Sind Sie sicher, dass Sie fortfahren möchten?" msgstr "Sie sind dabei, dieses Dokument an die Empfänger zu senden. Sind Sie sicher, dass Sie fortfahren möchten?"

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: de\n" "Language: de\n"
"Project-Id-Version: documenso-app\n" "Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-08-27 16:03\n" "PO-Revision-Date: 2024-09-05 06:04\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: German\n" "Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@ -424,6 +424,8 @@ msgstr ""
#: apps/marketing/src/components/(marketing)/i18n-switcher.tsx:47 #: apps/marketing/src/components/(marketing)/i18n-switcher.tsx:47
msgid "Search languages..." msgid "Search languages..."
msgstr "" msgstr ""
#~ msgid "Search languages..."
#~ msgstr "Sprachen suchen..."
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:109 #: apps/marketing/src/app/(marketing)/pricing/page.tsx:109
msgid "Securely. Our data centers are located in Frankfurt (Germany), giving us the best local privacy laws. We are very aware of the sensitive nature of our data and follow best practices to ensure the security and integrity of the data entrusted to us." msgid "Securely. Our data centers are located in Frankfurt (Germany), giving us the best local privacy laws. We are very aware of the sensitive nature of our data and follow best practices to ensure the security and integrity of the data entrusted to us."

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -111,7 +111,7 @@ msgid "Advanced Options"
msgstr "Advanced Options" msgstr "Advanced Options"
#: packages/ui/primitives/document-flow/add-fields.tsx:510 #: packages/ui/primitives/document-flow/add-fields.tsx:510
#: packages/ui/primitives/template-flow/add-template-fields.tsx:370 #: packages/ui/primitives/template-flow/add-template-fields.tsx:402
msgid "Advanced settings" msgid "Advanced settings"
msgstr "Advanced settings" msgstr "Advanced settings"
@ -135,7 +135,15 @@ msgstr "Approver"
msgid "Approving" msgid "Approving"
msgstr "Approving" msgstr "Approving"
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:297 #: packages/ui/primitives/signature-pad/signature-pad.tsx:276
msgid "Black"
msgstr "Black"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:290
msgid "Blue"
msgstr "Blue"
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:287
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:58 #: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:58
msgid "Cancel" msgid "Cancel"
msgstr "Cancel" msgstr "Cancel"
@ -162,7 +170,7 @@ msgid "Character Limit"
msgstr "Character Limit" msgstr "Character Limit"
#: packages/ui/primitives/document-flow/add-fields.tsx:932 #: packages/ui/primitives/document-flow/add-fields.tsx:932
#: packages/ui/primitives/template-flow/add-template-fields.tsx:756 #: packages/ui/primitives/template-flow/add-template-fields.tsx:788
msgid "Checkbox" msgid "Checkbox"
msgstr "Checkbox" msgstr "Checkbox"
@ -174,7 +182,7 @@ msgstr "Checkbox values"
msgid "Clear filters" msgid "Clear filters"
msgstr "Clear filters" msgstr "Clear filters"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:256 #: packages/ui/primitives/signature-pad/signature-pad.tsx:310
msgid "Clear Signature" msgid "Clear Signature"
msgstr "Clear Signature" msgstr "Clear Signature"
@ -191,7 +199,7 @@ msgid "Configure Direct Recipient"
msgstr "Configure Direct Recipient" msgstr "Configure Direct Recipient"
#: packages/ui/primitives/document-flow/add-fields.tsx:511 #: packages/ui/primitives/document-flow/add-fields.tsx:511
#: packages/ui/primitives/template-flow/add-template-fields.tsx:371 #: packages/ui/primitives/template-flow/add-template-fields.tsx:403
msgid "Configure the {0} field" msgid "Configure the {0} field"
msgstr "Configure the {0} field" msgstr "Configure the {0} field"
@ -208,7 +216,7 @@ msgid "Custom Text"
msgstr "Custom Text" msgstr "Custom Text"
#: packages/ui/primitives/document-flow/add-fields.tsx:828 #: packages/ui/primitives/document-flow/add-fields.tsx:828
#: packages/ui/primitives/template-flow/add-template-fields.tsx:652 #: packages/ui/primitives/template-flow/add-template-fields.tsx:684
msgid "Date" msgid "Date"
msgstr "Date" msgstr "Date"
@ -240,7 +248,7 @@ msgid "Drag & drop your PDF here."
msgstr "Drag & drop your PDF here." msgstr "Drag & drop your PDF here."
#: packages/ui/primitives/document-flow/add-fields.tsx:958 #: packages/ui/primitives/document-flow/add-fields.tsx:958
#: packages/ui/primitives/template-flow/add-template-fields.tsx:782 #: packages/ui/primitives/template-flow/add-template-fields.tsx:814
msgid "Dropdown" msgid "Dropdown"
msgstr "Dropdown" msgstr "Dropdown"
@ -252,7 +260,7 @@ msgstr "Dropdown options"
#: packages/ui/primitives/document-flow/add-signature.tsx:272 #: packages/ui/primitives/document-flow/add-signature.tsx:272
#: packages/ui/primitives/document-flow/add-signers.tsx:232 #: packages/ui/primitives/document-flow/add-signers.tsx:232
#: packages/ui/primitives/document-flow/add-signers.tsx:239 #: packages/ui/primitives/document-flow/add-signers.tsx:239
#: packages/ui/primitives/template-flow/add-template-fields.tsx:600 #: packages/ui/primitives/template-flow/add-template-fields.tsx:632
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:210 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:210
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:217 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:217
msgid "Email" msgid "Email"
@ -270,7 +278,7 @@ msgstr "Enable Direct Link Signing"
msgid "Enter password" msgid "Enter password"
msgstr "Enter password" msgstr "Enter password"
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:226 #: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:216
msgid "Error" msgid "Error"
msgstr "Error" msgstr "Error"
@ -279,7 +287,7 @@ msgstr "Error"
msgid "External ID" msgid "External ID"
msgstr "External ID" msgstr "External ID"
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:227 #: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:217
msgid "Failed to save settings." msgid "Failed to save settings."
msgstr "Failed to save settings." msgstr "Failed to save settings."
@ -307,6 +315,10 @@ msgstr "Global recipient action authentication"
msgid "Go Back" msgid "Go Back"
msgstr "Go Back" msgstr "Go Back"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:297
msgid "Green"
msgstr "Green"
#: packages/lib/constants/recipient-roles.ts:72 #: packages/lib/constants/recipient-roles.ts:72
msgid "I am a signer of this document" msgid "I am a signer of this document"
msgstr "I am a signer of this document" msgstr "I am a signer of this document"
@ -362,7 +374,7 @@ msgstr "Min"
#: packages/ui/primitives/document-flow/add-fields.tsx:802 #: packages/ui/primitives/document-flow/add-fields.tsx:802
#: packages/ui/primitives/document-flow/add-signature.tsx:298 #: packages/ui/primitives/document-flow/add-signature.tsx:298
#: packages/ui/primitives/document-flow/add-signers.tsx:265 #: packages/ui/primitives/document-flow/add-signers.tsx:265
#: packages/ui/primitives/template-flow/add-template-fields.tsx:626 #: packages/ui/primitives/template-flow/add-template-fields.tsx:658
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:245 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:245
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:251 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:251
msgid "Name" msgid "Name"
@ -381,12 +393,12 @@ msgid "Needs to view"
msgstr "Needs to view" msgstr "Needs to view"
#: packages/ui/primitives/document-flow/add-fields.tsx:613 #: packages/ui/primitives/document-flow/add-fields.tsx:613
#: packages/ui/primitives/template-flow/add-template-fields.tsx:465 #: packages/ui/primitives/template-flow/add-template-fields.tsx:497
msgid "No recipient matching this description was found." msgid "No recipient matching this description was found."
msgstr "No recipient matching this description was found." msgstr "No recipient matching this description was found."
#: packages/ui/primitives/document-flow/add-fields.tsx:629 #: packages/ui/primitives/document-flow/add-fields.tsx:629
#: packages/ui/primitives/template-flow/add-template-fields.tsx:481 #: packages/ui/primitives/template-flow/add-template-fields.tsx:513
msgid "No recipients with this role" msgid "No recipients with this role"
msgstr "No recipients with this role" msgstr "No recipients with this role"
@ -411,7 +423,7 @@ msgid "No value found."
msgstr "No value found." msgstr "No value found."
#: packages/ui/primitives/document-flow/add-fields.tsx:880 #: packages/ui/primitives/document-flow/add-fields.tsx:880
#: packages/ui/primitives/template-flow/add-template-fields.tsx:704 #: packages/ui/primitives/template-flow/add-template-fields.tsx:736
msgid "Number" msgid "Number"
msgstr "Number" msgstr "Number"
@ -446,7 +458,7 @@ msgid "Placeholder"
msgstr "Placeholder" msgstr "Placeholder"
#: packages/ui/primitives/document-flow/add-fields.tsx:906 #: packages/ui/primitives/document-flow/add-fields.tsx:906
#: packages/ui/primitives/template-flow/add-template-fields.tsx:730 #: packages/ui/primitives/template-flow/add-template-fields.tsx:762
msgid "Radio" msgid "Radio"
msgstr "Radio" msgstr "Radio"
@ -472,6 +484,10 @@ msgstr "Receives copy"
msgid "Recipient action authentication" msgid "Recipient action authentication"
msgstr "Recipient action authentication" msgstr "Recipient action authentication"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:283
msgid "Red"
msgstr "Red"
#: packages/ui/primitives/document-flow/add-settings.tsx:298 #: packages/ui/primitives/document-flow/add-settings.tsx:298
#: packages/ui/primitives/template-flow/add-template-settings.tsx:332 #: packages/ui/primitives/template-flow/add-template-settings.tsx:332
msgid "Redirect URL" msgid "Redirect URL"
@ -493,14 +509,18 @@ msgstr "Required field"
msgid "Rows per page" msgid "Rows per page"
msgstr "Rows per page" msgstr "Rows per page"
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:296 #: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:286
msgid "Save" msgid "Save"
msgstr "Save" msgstr "Save"
#: packages/ui/primitives/template-flow/add-template-fields.tsx:798 #: packages/ui/primitives/template-flow/add-template-fields.tsx:848
msgid "Save Template" msgid "Save Template"
msgstr "Save Template" msgstr "Save Template"
#: packages/ui/components/common/language-switcher-dialog.tsx:34
msgid "Search languages..."
msgstr "Search languages..."
#: packages/ui/primitives/document-flow/field-items-advanced-settings/dropdown-field.tsx:105 #: packages/ui/primitives/document-flow/field-items-advanced-settings/dropdown-field.tsx:105
msgid "Select" msgid "Select"
msgstr "Select" msgstr "Select"
@ -547,7 +567,7 @@ msgstr "Sign"
#: packages/ui/primitives/document-flow/add-fields.tsx:724 #: packages/ui/primitives/document-flow/add-fields.tsx:724
#: packages/ui/primitives/document-flow/add-signature.tsx:323 #: packages/ui/primitives/document-flow/add-signature.tsx:323
#: packages/ui/primitives/document-flow/field-icon.tsx:52 #: packages/ui/primitives/document-flow/field-icon.tsx:52
#: packages/ui/primitives/template-flow/add-template-fields.tsx:548 #: packages/ui/primitives/template-flow/add-template-fields.tsx:580
msgid "Signature" msgid "Signature"
msgstr "Signature" msgstr "Signature"
@ -593,7 +613,7 @@ msgid "Template title"
msgstr "Template title" msgstr "Template title"
#: packages/ui/primitives/document-flow/add-fields.tsx:854 #: packages/ui/primitives/document-flow/add-fields.tsx:854
#: packages/ui/primitives/template-flow/add-template-fields.tsx:678 #: packages/ui/primitives/template-flow/add-template-fields.tsx:710
msgid "Text" msgid "Text"
msgstr "Text" msgstr "Text"
@ -683,6 +703,7 @@ msgid "Title"
msgstr "Title" msgstr "Title"
#: packages/ui/primitives/document-flow/add-fields.tsx:971 #: packages/ui/primitives/document-flow/add-fields.tsx:971
#: packages/ui/primitives/template-flow/add-template-fields.tsx:828
msgid "To proceed further, please set at least one value for the {0} field." msgid "To proceed further, please set at least one value for the {0} field."
msgstr "To proceed further, please set at least one value for the {0} field." msgstr "To proceed further, please set at least one value for the {0} field."
@ -728,6 +749,10 @@ msgstr "Viewer"
msgid "Viewing" msgid "Viewing"
msgstr "Viewing" msgstr "Viewing"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:280
#~ msgid "White"
#~ msgstr "White"
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:44 #: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:44
msgid "You are about to send this document to the recipients. Are you sure you want to continue?" msgid "You are about to send this document to the recipients. Are you sure you want to continue?"
msgstr "You are about to send this document to the recipients. Are you sure you want to continue?" msgstr "You are about to send this document to the recipients. Are you sure you want to continue?"

File diff suppressed because one or more lines are too long

View File

@ -425,8 +425,8 @@ msgid "Save $60 or $120"
msgstr "Save $60 or $120" msgstr "Save $60 or $120"
#: apps/marketing/src/components/(marketing)/i18n-switcher.tsx:47 #: apps/marketing/src/components/(marketing)/i18n-switcher.tsx:47
msgid "Search languages..." #~ msgid "Search languages..."
msgstr "Search languages..." #~ msgstr "Search languages..."
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:109 #: apps/marketing/src/app/(marketing)/pricing/page.tsx:109
msgid "Securely. Our data centers are located in Frankfurt (Germany), giving us the best local privacy laws. We are very aware of the sensitive nature of our data and follow best practices to ensure the security and integrity of the data entrusted to us." msgid "Securely. Our data centers are located in Frankfurt (Germany), giving us the best local privacy laws. We are very aware of the sensitive nature of our data and follow best practices to ensure the security and integrity of the data entrusted to us."

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -2,8 +2,8 @@ import type { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension
import type { I18n } from '@lingui/core'; import type { I18n } from '@lingui/core';
import { IS_APP_WEB } from '../constants/app'; import { IS_APP_WEB, IS_APP_WEB_I18N_ENABLED } from '../constants/app';
import type { SupportedLanguageCodes } from '../constants/i18n'; import type { I18nLocaleData, SupportedLanguageCodes } from '../constants/i18n';
import { APP_I18N_OPTIONS } from '../constants/i18n'; import { APP_I18N_OPTIONS } from '../constants/i18n';
export async function dynamicActivate(i18nInstance: I18n, locale: string) { export async function dynamicActivate(i18nInstance: I18n, locale: string) {
@ -14,48 +14,58 @@ export async function dynamicActivate(i18nInstance: I18n, locale: string) {
i18nInstance.loadAndActivate({ locale, messages }); i18nInstance.loadAndActivate({ locale, messages });
} }
/** const parseLanguageFromLocale = (locale: string): SupportedLanguageCodes | null => {
* Extract the language if supported from the cookies header. const [language, _country] = locale.split('-');
*
* Returns `null` if not supported or not found.
*/
export const extractSupportedLanguageFromCookies = (
cookies: ReadonlyRequestCookies,
): SupportedLanguageCodes | null => {
const preferredLanguage = cookies.get('i18n');
const foundSupportedLanguage = APP_I18N_OPTIONS.supportedLangs.find(
(lang): lang is SupportedLanguageCodes => lang === preferredLanguage?.value,
);
return foundSupportedLanguage || null;
};
/**
* Extracts the language from the `accept-language` header.
*
* Returns `null` if not supported or not found.
*/
export const extractSupportedLanguageFromHeaders = (
headers: Headers,
): SupportedLanguageCodes | null => {
const locales = headers.get('accept-language') ?? '';
const [locale] = locales.split(',');
// Convert locale to language.
const [language] = locale.split('-');
const foundSupportedLanguage = APP_I18N_OPTIONS.supportedLangs.find( const foundSupportedLanguage = APP_I18N_OPTIONS.supportedLangs.find(
(lang): lang is SupportedLanguageCodes => lang === language, (lang): lang is SupportedLanguageCodes => lang === language,
); );
return foundSupportedLanguage || null; if (!foundSupportedLanguage) {
return null;
}
return foundSupportedLanguage;
}; };
type ExtractSupportedLanguageOptions = { /**
headers?: Headers; * Extract the language if supported from the cookies header.
cookies?: ReadonlyRequestCookies; *
* Returns `null` if not supported or not found.
*/
export const extractLocaleDataFromCookies = (
cookies: ReadonlyRequestCookies,
): SupportedLanguageCodes | null => {
const preferredLocale = cookies.get('language')?.value || '';
const language = parseLanguageFromLocale(preferredLocale || '');
if (!language) {
return null;
}
return language;
};
/**
* Extracts the language from the `accept-language` header.
*/
export const extractLocaleDataFromHeaders = (
headers: Headers,
): { lang: SupportedLanguageCodes | null; locales: string[] } => {
const headerLocales = (headers.get('accept-language') ?? '').split(',');
const language = parseLanguageFromLocale(headerLocales[0]);
return {
lang: language,
locales: [headerLocales[0]],
};
};
type ExtractLocaleDataOptions = {
headers: Headers;
cookies: ReadonlyRequestCookies;
}; };
/** /**
@ -63,25 +73,25 @@ type ExtractSupportedLanguageOptions = {
* *
* Will return the default fallback language if not found. * Will return the default fallback language if not found.
*/ */
export const extractSupportedLanguage = ({ export const extractLocaleData = ({
headers, headers,
cookies, cookies,
}: ExtractSupportedLanguageOptions): SupportedLanguageCodes => { }: ExtractLocaleDataOptions): I18nLocaleData => {
if (cookies) { let lang: SupportedLanguageCodes | null = extractLocaleDataFromCookies(cookies);
const langCookie = extractSupportedLanguageFromCookies(cookies);
if (langCookie) { const langHeader = extractLocaleDataFromHeaders(headers);
return langCookie;
} if (!lang && langHeader?.lang) {
lang = langHeader.lang;
} }
if (headers) { // Override web app to be English.
const langHeader = extractSupportedLanguageFromHeaders(headers); if (!IS_APP_WEB_I18N_ENABLED && IS_APP_WEB) {
lang = 'en';
if (langHeader) {
return langHeader;
}
} }
return APP_I18N_OPTIONS.sourceLang; return {
lang: lang || APP_I18N_OPTIONS.sourceLang,
locales: langHeader.locales,
};
}; };

View File

@ -28,9 +28,18 @@ export const signWithLocalCert = async ({ pdf }: SignWithLocalCertOptions) => {
} }
if (!cert) { if (!cert) {
cert = Buffer.from( let certPath = process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH || '/opt/documenso/cert.p12';
fs.readFileSync(process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH || './example/cert.p12'),
); // We don't want to make the development server suddenly crash when using the `dx` script
// so we retain this when NODE_ENV isn't set to production which it should be in most production
// deployments.
//
// Our docker image automatically sets this so it shouldn't be an issue for self-hosters.
if (process.env.NODE_ENV !== 'production') {
certPath = process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH || './example/cert.p12';
}
cert = Buffer.from(fs.readFileSync(certPath));
} }
const signature = signWithP12({ const signature = signWithP12({

View File

@ -0,0 +1,35 @@
import Link from 'next/link';
import { Button } from '../primitives/button';
import { Card, CardContent } from '../primitives/card';
type CallToActionProps = {
className?: string;
utmSource?: string;
};
export const CallToAction = ({ className, utmSource = 'generic-cta' }: CallToActionProps) => {
return (
<Card spotlight className={className}>
<CardContent className="flex flex-col items-center justify-center p-12">
<h2 className="text-center text-2xl font-bold">Looking for the managed solution?</h2>
<p className="text-muted-foreground mt-4 max-w-[55ch] text-center leading-normal">
You can get started with Documenso in minutes. We handle the infrastructure, so you can
focus on signing documents.
</p>
<Button
className="focus-visible:ring-ring ring-offset-background bg-primary text-primary-foreground hover:bg-primary/90text-sm mt-8 inline-flex items-center justify-center rounded-full border font-medium no-underline transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
variant="default"
size="lg"
asChild
>
<Link href={`https://app.documenso.com/signup?utm_source=${utmSource}`} target="_blank">
Get started
</Link>
</Button>
</CardContent>
</Card>
);
};

View File

@ -0,0 +1,57 @@
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { CheckIcon } from 'lucide-react';
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { switchI18NLanguage } from '@documenso/lib/server-only/i18n/switch-i18n-language';
import { dynamicActivate } from '@documenso/lib/utils/i18n';
import { cn } from '@documenso/ui/lib/utils';
import {
CommandDialog,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@documenso/ui/primitives/command';
type LanguageSwitcherDialogProps = {
open: boolean;
setOpen: (_open: boolean) => void;
};
export const LanguageSwitcherDialog = ({ open, setOpen }: LanguageSwitcherDialogProps) => {
const { i18n, _ } = useLingui();
const setLanguage = async (lang: string) => {
setOpen(false);
await dynamicActivate(i18n, lang);
await switchI18NLanguage(lang);
};
return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder={_(msg`Search languages...`)} />
<CommandList>
<CommandGroup>
{Object.values(SUPPORTED_LANGUAGES).map((language) => (
<CommandItem
key={language.short}
value={language.full}
onSelect={async () => setLanguage(language.short)}
>
<CheckIcon
className={cn(
'mr-2 h-4 w-4',
i18n.locale === language.short ? 'opacity-100' : 'opacity-0',
)}
/>
{SUPPORTED_LANGUAGES[language.short].full}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</CommandDialog>
);
};

View File

@ -155,17 +155,7 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
const doesFieldExist = (!!document || !!template) && field.nativeId !== undefined; const doesFieldExist = (!!document || !!template) && field.nativeId !== undefined;
const { data: fieldData } = trpc.field.getField.useQuery( const fieldMeta = field?.fieldMeta;
{
fieldId: Number(field.nativeId),
teamId,
},
{
enabled: doesFieldExist,
},
);
const fieldMeta = fieldData?.fieldMeta;
const localStorageKey = `field_${field.formId}_${field.type}`; const localStorageKey = `field_${field.formId}_${field.type}`;

View File

@ -9,6 +9,13 @@ import type { StrokeOptions } from 'perfect-freehand';
import { getStroke } from 'perfect-freehand'; import { getStroke } from 'perfect-freehand';
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once'; import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
import { cn } from '../../lib/utils'; import { cn } from '../../lib/utils';
import { getSvgPathFromStroke } from './helper'; import { getSvgPathFromStroke } from './helper';
@ -36,6 +43,7 @@ export const SignaturePad = ({
const [isPressed, setIsPressed] = useState(false); const [isPressed, setIsPressed] = useState(false);
const [lines, setLines] = useState<Point[][]>([]); const [lines, setLines] = useState<Point[][]>([]);
const [currentLine, setCurrentLine] = useState<Point[]>([]); const [currentLine, setCurrentLine] = useState<Point[]>([]);
const [selectedColor, setSelectedColor] = useState('black');
const perfectFreehandOptions = useMemo(() => { const perfectFreehandOptions = useMemo(() => {
const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10; const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10;
@ -85,6 +93,7 @@ export const SignaturePad = ({
ctx.restore(); ctx.restore();
ctx.imageSmoothingEnabled = true; ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high'; ctx.imageSmoothingQuality = 'high';
ctx.fillStyle = selectedColor;
lines.forEach((line) => { lines.forEach((line) => {
const pathData = new Path2D( const pathData = new Path2D(
@ -129,6 +138,7 @@ export const SignaturePad = ({
ctx.imageSmoothingEnabled = true; ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high'; ctx.imageSmoothingQuality = 'high';
ctx.fillStyle = selectedColor;
newLines.forEach((line) => { newLines.forEach((line) => {
const pathData = new Path2D( const pathData = new Path2D(
@ -237,7 +247,13 @@ export const SignaturePad = ({
> >
<canvas <canvas
ref={$el} ref={$el}
className={cn('relative block dark:invert', className)} className={cn(
'relative block',
{
'dark:hue-rotate-180 dark:invert': selectedColor === 'black',
},
className,
)}
style={{ touchAction: 'none' }} style={{ touchAction: 'none' }}
onPointerMove={(event) => onMouseMove(event)} onPointerMove={(event) => onMouseMove(event)}
onPointerDown={(event) => onMouseDown(event)} onPointerDown={(event) => onMouseDown(event)}
@ -247,6 +263,44 @@ export const SignaturePad = ({
{...props} {...props}
/> />
<div className="text-foreground absolute right-2 top-2 filter">
<Select defaultValue={selectedColor} onValueChange={(value) => setSelectedColor(value)}>
<SelectTrigger className="h-auto w-auto border-none p-1">
<SelectValue placeholder="" />
</SelectTrigger>
<SelectContent className="w-[150px]" align="end">
<SelectItem value="black">
<div className="text-muted-foreground flex items-center px-1 text-sm">
<div className="border-border mr-2 h-5 w-5 rounded-full border-2 bg-black shadow-sm" />
<Trans>Black</Trans>
</div>
</SelectItem>
<SelectItem value="red">
<div className="text-muted-foreground flex items-center px-1 text-sm">
<div className="border-border mr-2 h-5 w-5 rounded-full border-2 bg-[red] shadow-sm" />
<Trans>Red</Trans>
</div>
</SelectItem>
<SelectItem value="blue">
<div className="text-muted-foreground flex items-center px-1 text-sm">
<div className="border-border mr-2 h-5 w-5 rounded-full border-2 bg-[blue] shadow-sm" />
<Trans>Blue</Trans>
</div>
</SelectItem>
<SelectItem value="green">
<div className="text-muted-foreground flex items-center px-1 text-sm">
<div className="border-border mr-2 h-5 w-5 rounded-full border-2 bg-[green] shadow-sm" />
<Trans>Green</Trans>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="absolute bottom-4 right-4 flex gap-2"> <div className="absolute bottom-4 right-4 flex gap-2">
<button <button
type="button" type="button"

View File

@ -157,6 +157,38 @@ export const AddTemplateFieldsFormPartial = ({
selectedSignerIndex === -1 ? 0 : selectedSignerIndex, selectedSignerIndex === -1 ? 0 : selectedSignerIndex,
); );
const filterFieldsWithEmptyValues = (fields: typeof localFields, fieldType: string) =>
fields
.filter((field) => field.type === fieldType)
.filter((field) => {
if (field.fieldMeta && 'values' in field.fieldMeta) {
return field.fieldMeta.values?.length === 0;
}
return true;
});
const emptyCheckboxFields = useMemo(
() => filterFieldsWithEmptyValues(localFields, FieldType.CHECKBOX),
// eslint-disable-next-line react-hooks/exhaustive-deps
[localFields],
);
const emptyRadioFields = useMemo(
() => filterFieldsWithEmptyValues(localFields, FieldType.RADIO),
// eslint-disable-next-line react-hooks/exhaustive-deps
[localFields],
);
const emptySelectFields = useMemo(
() => filterFieldsWithEmptyValues(localFields, FieldType.DROPDOWN),
// eslint-disable-next-line react-hooks/exhaustive-deps
[localFields],
);
const hasErrors =
emptyCheckboxFields.length > 0 || emptyRadioFields.length > 0 || emptySelectFields.length > 0;
const [isFieldWithinBounds, setIsFieldWithinBounds] = useState(false); const [isFieldWithinBounds, setIsFieldWithinBounds] = useState(false);
const [coords, setCoords] = useState({ const [coords, setCoords] = useState({
x: 0, x: 0,
@ -789,6 +821,24 @@ export const AddTemplateFieldsFormPartial = ({
</div> </div>
</DocumentFlowFormContainerContent> </DocumentFlowFormContainerContent>
{hasErrors && (
<div className="mt-4">
<ul>
<li className="text-sm text-red-500">
<Trans>
To proceed further, please set at least one value for the{' '}
{emptyCheckboxFields.length > 0
? 'Checkbox'
: emptyRadioFields.length > 0
? 'Radio'
: 'Select'}{' '}
field.
</Trans>
</li>
</ul>
</div>
)}
<DocumentFlowFormContainerFooter> <DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep step={currentStep} maxStep={totalSteps} /> <DocumentFlowFormContainerStep step={currentStep} maxStep={totalSteps} />
@ -796,6 +846,7 @@ export const AddTemplateFieldsFormPartial = ({
loading={isSubmitting} loading={isSubmitting}
disabled={isSubmitting} disabled={isSubmitting}
goNextLabel={msg`Save Template`} goNextLabel={msg`Save Template`}
disableNextStep={hasErrors}
onGoBackClick={() => { onGoBackClick={() => {
previousStep(); previousStep();
remove(); remove();

View File

@ -43,6 +43,8 @@ services:
envVarKey: RENDER_EXTERNAL_URL envVarKey: RENDER_EXTERNAL_URL
- key: NEXT_PUBLIC_MARKETING_URL - key: NEXT_PUBLIC_MARKETING_URL
value: 'http://localhost:3001' value: 'http://localhost:3001'
- key: NEXT_PRIVATE_INTERNAL_WEBAPP_URL
value: 'http://localhost:10000'
# SMTP # SMTP
- key: NEXT_PRIVATE_SMTP_TRANSPORT - key: NEXT_PRIVATE_SMTP_TRANSPORT

View File

@ -48,6 +48,7 @@
"NEXT_PUBLIC_PROJECT", "NEXT_PUBLIC_PROJECT",
"NEXT_PUBLIC_WEBAPP_URL", "NEXT_PUBLIC_WEBAPP_URL",
"NEXT_PUBLIC_MARKETING_URL", "NEXT_PUBLIC_MARKETING_URL",
"NEXT_PRIVATE_INTERNAL_WEBAPP_URL",
"NEXT_PUBLIC_POSTHOG_KEY", "NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_FEATURE_BILLING_ENABLED", "NEXT_PUBLIC_FEATURE_BILLING_ENABLED",
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID", "NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID",