diff --git a/template/app/src/client/components/NavBar/contentSections.ts b/template/app/src/client/components/NavBar/contentSections.ts
index 907ed1a3..fbf2abec 100644
--- a/template/app/src/client/components/NavBar/contentSections.ts
+++ b/template/app/src/client/components/NavBar/contentSections.ts
@@ -1,11 +1,35 @@
import type { NavigationItem } from '../NavBar/NavBar';
import { routes } from 'wasp/client/router';
import { BlogUrl, DocsUrl } from '../../../shared/common';
+import { useTranslation } from 'react-i18next';
-export const appNavigationItems: NavigationItem[] = [
- { name: 'AI Scheduler (Demo App)', to: routes.DemoAppRoute.to },
- { name: 'File Upload (AWS S3)', to: routes.FileUploadRoute.to },
- { name: 'Pricing', to: routes.PricingPageRoute.to },
- { name: 'Documentation', to: DocsUrl },
- { name: 'Blog', to: BlogUrl },
-];
+export const useAppNavigationItems = (): NavigationItem[] => {
+ const { t, i18n } = useTranslation();
+
+ // Debug log
+ console.log('Current language:', i18n.language);
+ console.log('Translation test:', t('navigation.aiScheduler'));
+
+ return [
+ {
+ name: t('navigation.aiScheduler'),
+ to: routes.DemoAppRoute.to
+ },
+ {
+ name: t('navigation.fileUpload'),
+ to: routes.FileUploadRoute.to
+ },
+ {
+ name: t('navigation.pricing'),
+ to: routes.PricingPageRoute.to
+ },
+ {
+ name: t('navigation.documentation'),
+ to: DocsUrl
+ },
+ {
+ name: t('navigation.blog'),
+ to: BlogUrl
+ },
+ ];
+};
diff --git a/template/app/src/demo-ai-app/DemoAppPage.tsx b/template/app/src/demo-ai-app/DemoAppPage.tsx
index a14dc546..64823f5a 100644
--- a/template/app/src/demo-ai-app/DemoAppPage.tsx
+++ b/template/app/src/demo-ai-app/DemoAppPage.tsx
@@ -14,95 +14,96 @@ import { CgSpinner } from 'react-icons/cg';
import { TiDelete } from 'react-icons/ti';
import type { GeneratedSchedule, MainTask, SubTask } from './schedule';
import { cn } from '../client/cn';
+import { useTranslation } from 'react-i18next';
export default function DemoAppPage() {
+ const { t } = useTranslation();
+
return (
- AI Day Scheduler
+ AI {t('aiScheduler.title')}
- This example app uses OpenAI's chat completions with function calling to return a structured JSON object. Try
- it out, enter your day's tasks, and let AI do the rest!
+ {t('aiScheduler.description')}
@@ -348,16 +348,14 @@ function MainTaskTable({ mainTask, subtasks }: { mainTask: MainTask; subtasks: S
return (
-
+
@@ -366,23 +364,31 @@ function MainTaskTable({ mainTask, subtasks }: { mainTask: MainTask; subtasks: S
}
})
) : (
-
OpenAI didn't return any Subtasks. Try again.
+
{t('aiScheduler.taskTable.noSubtasks')}
)}
>
);
}
function SubtaskTable({ description, time }: { description: string; time: number }) {
+ const { t } = useTranslation();
const [isDone, setIsDone] = useState(false);
const convertHrsToMinutes = (time: number) => {
if (time === 0) return 0;
const hours = Math.floor(time);
const minutes = Math.round((time - hours) * 60);
- return `${hours > 0 ? hours + 'hr' : ''} ${minutes > 0 ? minutes + 'min' : ''}`;
+
+ if (hours > 0 && minutes > 0) {
+ return t('aiScheduler.timeFormat.timeDisplay', { hours, minutes });
+ } else if (hours > 0) {
+ return t('aiScheduler.timeFormat.hourOnly', { hours });
+ } else {
+ return t('aiScheduler.timeFormat.minuteOnly', { minutes });
+ }
};
- const minutes = useMemo(() => convertHrsToMinutes(time), [time]);
+ const minutes = useMemo(() => convertHrsToMinutes(time), [time, t]);
return (
<>
@@ -391,19 +397,16 @@ function SubtaskTable({ description, time }: { description: string; time: number
className='ml-1 form-checkbox bg-purple-500 checked:bg-purple-300 rounded border-purple-600 duration-200 ease-in-out hover:bg-purple-600 hover:checked:bg-purple-600 focus:ring focus:ring-purple-300 focus:checked:bg-purple-400 focus:ring-opacity-50'
checked={isDone}
onChange={(e) => setIsDone(e.currentTarget.checked)}
+ aria-label={t('aiScheduler.todo.checkbox.ariaLabel')}
/>
-
+
{description}
-
+
{minutes}
>
diff --git a/template/app/src/file-upload/FileUploadPage.tsx b/template/app/src/file-upload/FileUploadPage.tsx
index 74c71458..480cea56 100644
--- a/template/app/src/file-upload/FileUploadPage.tsx
+++ b/template/app/src/file-upload/FileUploadPage.tsx
@@ -1,37 +1,28 @@
-import { cn } from '../client/cn';
+import { createFile, useQuery, getAllFilesByUser, getDownloadFileSignedURL } from 'wasp/client/operations';
+import axios from 'axios';
import { useState, useEffect, FormEvent } from 'react';
-import type { File } from 'wasp/entities';
-import { useQuery, getAllFilesByUser, getDownloadFileSignedURL } from 'wasp/client/operations';
-import { type FileUploadError, uploadFileWithProgress, validateFile, ALLOWED_FILE_TYPES } from './fileUploading';
+import { cn } from '../client/cn';
+import { useTranslation } from 'react-i18next';
export default function FileUploadPage() {
- const [fileKeyForS3, setFileKeyForS3] = useState('');
- const [uploadProgressPercent, setUploadProgressPercent] = useState(0);
- const [uploadError, setUploadError] = useState(null);
+ const { t } = useTranslation();
+ const [fileToDownload, setFileToDownload] = useState('');
- const allUserFiles = useQuery(getAllFilesByUser, undefined, {
- // We disable automatic refetching because otherwise files would be refetched after `createFile` is called and the S3 URL is returned,
- // which happens before the file is actually fully uploaded. Instead, we manually (re)fetch on mount and after the upload is complete.
- enabled: false,
- });
+ const { data: files, error: filesError, isLoading: isFilesLoading } = useQuery(getAllFilesByUser);
const { isLoading: isDownloadUrlLoading, refetch: refetchDownloadUrl } = useQuery(
getDownloadFileSignedURL,
- { key: fileKeyForS3 },
+ { key: fileToDownload },
{ enabled: false }
);
useEffect(() => {
- allUserFiles.refetch();
- }, []);
-
- useEffect(() => {
- if (fileKeyForS3.length > 0) {
+ if (fileToDownload.length > 0) {
refetchDownloadUrl()
.then((urlQuery) => {
switch (urlQuery.status) {
case 'error':
console.error('Error fetching download URL', urlQuery.error);
- alert('Error fetching download');
+ alert(t('fileUpload.errors.downloadError.fetchFailed'));
return;
case 'success':
window.open(urlQuery.data, '_blank');
@@ -39,49 +30,38 @@ export default function FileUploadPage() {
}
})
.finally(() => {
- setFileKeyForS3('');
+ setFileToDownload('');
});
}
- }, [fileKeyForS3]);
+ }, [fileToDownload, t]);
const handleUpload = async (e: FormEvent) => {
try {
e.preventDefault();
-
- const formElement = e.target;
- if (!(formElement instanceof HTMLFormElement)) {
- throw new Error('Event target is not a form element');
+ const formData = new FormData(e.target as HTMLFormElement);
+ const file = formData.get('file-upload') as File;
+ if (!file || !file.name || !file.type) {
+ throw new Error(t('fileUpload.errors.uploadError.noFile'));
}
- const formData = new FormData(formElement);
- const file = formData.get('file-upload');
+ const fileType = file.type;
+ const name = file.name;
- if (!file || !(file instanceof File)) {
- setUploadError({
- message: 'Please select a file to upload.',
- code: 'NO_FILE',
- });
- return;
+ const { uploadUrl } = await createFile({ fileType, name });
+ if (!uploadUrl) {
+ throw new Error(t('fileUpload.errors.uploadError.uploadFailed'));
}
-
- const validationError = validateFile(file);
- if (validationError) {
- setUploadError(validationError);
- return;
+ const res = await axios.put(uploadUrl, file, {
+ headers: {
+ 'Content-Type': fileType,
+ },
+ });
+ if (res.status !== 200) {
+ throw new Error(t('fileUpload.errors.uploadError.s3Failed'));
}
-
- await uploadFileWithProgress({ file, setUploadProgressPercent });
- formElement.reset();
- allUserFiles.refetch();
} catch (error) {
- console.error('Error uploading file:', error);
- setUploadError({
- message:
- error instanceof Error ? error.message : 'An unexpected error occurred while uploading the file.',
- code: 'UPLOAD_FAILED',
- });
- } finally {
- setUploadProgressPercent(0);
+ alert(t('fileUpload.errors.uploadError.generic'));
+ console.error('Error uploading file', error);
}
};
@@ -90,75 +70,55 @@ export default function FileUploadPage() {
- AWS File Upload
+ AWS {t('fileUpload.title')}
- This is an example file upload page using AWS S3. Maybe your app needs this. Maybe it doesn't. But a
- lot of people asked for this feature, so here you go 🤝
+ {t('fileUpload.subtitle')}
- Choose between Stripe and LemonSqueezy as your payment provider. Just add your Product IDs! Try it
- out below with test credit card number
- 4242 4242 4242 4242 4242
+ {t('pricing.subtitle')}
+ {t('pricing.testCard')}