From 2e5a5d5e169e5d0040a5606c2ff91bea48fae47c Mon Sep 17 00:00:00 2001 From: arvinxx Date: Fri, 19 Jan 2024 17:28:36 +0800 Subject: [PATCH 1/3] :construction: wip: setting --- src/features/Settings.tsx | 18 +++++++++++------- src/services/Midjourney.ts | 8 ++++---- src/store/global/action.ts | 25 +++++++++++++++++++++++++ src/store/global/index.ts | 1 + src/store/global/initialState.ts | 16 ++++++++++++++++ src/store/global/selectors.ts | 7 +++++++ src/store/global/store.ts | 15 +++++++++++++++ src/store/midjourney/action.ts | 5 +---- 8 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 src/store/global/action.ts create mode 100644 src/store/global/index.ts create mode 100644 src/store/global/initialState.ts create mode 100644 src/store/global/selectors.ts create mode 100644 src/store/global/store.ts diff --git a/src/features/Settings.tsx b/src/features/Settings.tsx index 5f49d7e..64de86b 100644 --- a/src/features/Settings.tsx +++ b/src/features/Settings.tsx @@ -5,8 +5,9 @@ import { LucideSettings, Save } from 'lucide-react'; import Link from 'next/link'; import { memo, useState } from 'react'; import { Flexbox } from 'react-layout-kit'; +import { useGlobalStore } from 'src/store/global'; -import { useMidjourneyStore } from '@/store/midjourney'; +import { settingsSelectors } from '@/store/global/selectors'; const getErrorContent = (errorType: string | { type: string }) => { if (typeof errorType === 'string') return errorType; @@ -19,15 +20,18 @@ const getErrorContent = (errorType: string | { type: string }) => { return '网络请求错误'; }; + const Settings = memo(() => { - const [isSettingsModalOpen, MIDJOURNEY_API_URL, updateSettings] = useMidjourneyStore((s) => [ + const [isSettingsModalOpen, MIDJOURNEY_API_URL, updateSettings] = useGlobalStore((s) => [ s.isSettingsModalOpen, - s.settings.MIDJOURNEY_PROXY_URL, + settingsSelectors.proxyURL(s), s.updateSettings, ]); - const requestError = useMidjourneyStore((s) => s.requestError, isEqual); + + const requestError = useGlobalStore((s) => s.requestError, isEqual); const [url, setUrl] = useState(MIDJOURNEY_API_URL); + return ( <> { icon={} onClick={() => { updateSettings({ MIDJOURNEY_PROXY_URL: url }); - useMidjourneyStore.setState({ isSettingsModalOpen: false }); + useGlobalStore.setState({ isSettingsModalOpen: false }); }} type={'primary'} /> } onCancel={() => { - useMidjourneyStore.setState({ isSettingsModalOpen: false }); + useGlobalStore.setState({ isSettingsModalOpen: false }); }} open={isSettingsModalOpen} title={'Setting'} @@ -79,7 +83,7 @@ const Settings = memo(() => { { - useMidjourneyStore.setState({ isSettingsModalOpen: true }); + useGlobalStore.setState({ isSettingsModalOpen: true }); }} size={'site'} /> diff --git a/src/services/Midjourney.ts b/src/services/Midjourney.ts index d91e8ab..3de6a8c 100644 --- a/src/services/Midjourney.ts +++ b/src/services/Midjourney.ts @@ -1,4 +1,5 @@ -import { useMidjourneyStore } from '@/store/midjourney'; +import { useGlobalStore } from 'src/store/global'; + import { MidjourneyTask } from '@/types/task'; interface DescribeDTO { @@ -64,8 +65,7 @@ class MidjourneyService { body: data ? JSON.stringify(data) : undefined, headers: { 'Content-Type': 'application/json', - 'X-Midjourney-Proxy-Url': - useMidjourneyStore.getState().settings.MIDJOURNEY_PROXY_URL || '', + 'X-Midjourney-Proxy-Url': useGlobalStore.getState().settings.MIDJOURNEY_PROXY_URL || '', }, method, }); @@ -86,7 +86,7 @@ class MidjourneyService { /* empty */ } - useMidjourneyStore.setState({ + useGlobalStore.setState({ isSettingsModalOpen: true, requestError: { ...requestError, body }, }); diff --git a/src/store/global/action.ts b/src/store/global/action.ts new file mode 100644 index 0000000..79547e0 --- /dev/null +++ b/src/store/global/action.ts @@ -0,0 +1,25 @@ +import { StateCreator } from 'zustand'; + +import { storageService } from '@/services/storageService'; + +import { AppSettings, initialState } from './initialState'; +import { SettingsStore } from './store'; + +export interface StoreAction { + updateSettings: (settings: Partial) => void; +} + +export const actions: StateCreator< + SettingsStore, + [['zustand/devtools', never]], + [], + StoreAction +> = (set, get) => ({ + ...initialState, + + updateSettings: (settings) => { + set({ settings: { ...get().settings, ...settings } }); + + storageService.setSettings(settings); + }, +}); diff --git a/src/store/global/index.ts b/src/store/global/index.ts new file mode 100644 index 0000000..d406816 --- /dev/null +++ b/src/store/global/index.ts @@ -0,0 +1 @@ +export * from './store'; diff --git a/src/store/global/initialState.ts b/src/store/global/initialState.ts new file mode 100644 index 0000000..f63e137 --- /dev/null +++ b/src/store/global/initialState.ts @@ -0,0 +1,16 @@ +export interface AppSettings { + MIDJOURNEY_PROXY_URL?: string; +} + +export interface SettingsState { + isSettingsModalOpen: boolean; + requestError?: { body: string | { type: string }; message: string; status: number }; + + settings: AppSettings; +} + +export const initialState: SettingsState = { + isSettingsModalOpen: false, + + settings: {}, +}; diff --git a/src/store/global/selectors.ts b/src/store/global/selectors.ts new file mode 100644 index 0000000..1d32ffe --- /dev/null +++ b/src/store/global/selectors.ts @@ -0,0 +1,7 @@ +import { SettingsStore } from './store'; + +export const proxyURL = (s: SettingsStore) => s.settings.MIDJOURNEY_PROXY_URL; + +export const settingsSelectors = { + proxyURL, +}; diff --git a/src/store/global/store.ts b/src/store/global/store.ts new file mode 100644 index 0000000..248bb02 --- /dev/null +++ b/src/store/global/store.ts @@ -0,0 +1,15 @@ +import { devtools } from 'zustand/middleware'; +import { shallow } from 'zustand/shallow'; +import { createWithEqualityFn } from 'zustand/traditional'; + +import { StoreAction, actions } from './action'; +import { SettingsState, initialState } from './initialState'; + +export interface SettingsStore extends SettingsState, StoreAction {} + +export const useGlobalStore = createWithEqualityFn()( + devtools((...params) => ({ ...initialState, ...actions(...params) }), { + name: 'MidjourneyGlobalStore', + }), + shallow, +); diff --git a/src/store/midjourney/action.ts b/src/store/midjourney/action.ts index 8878044..ad82f80 100644 --- a/src/store/midjourney/action.ts +++ b/src/store/midjourney/action.ts @@ -185,10 +185,7 @@ export const actions: StateCreator< return mockState; } - const app = await storageService.getFromLocalStorage(); - const settings = await storageService.getFromLocalStorage(); - - return { ...app, settings }; + return await storageService.getFromLocalStorage(); } if (payload?.name === 'showMJ') { From db0d7f5e038915f1e016e7549debeeb52874a92c Mon Sep 17 00:00:00 2001 From: arvinxx Date: Sat, 20 Jan 2024 17:17:47 +0800 Subject: [PATCH 2/3] :sparkles: feat: support i18n locale --- .i18nrc.js | 27 +++++ locales/ar/common.json | 40 +++++++ locales/de-DE/common.json | 40 +++++++ locales/en-US/common.json | 40 +++++++ locales/es-ES/common.json | 40 +++++++ locales/fr-FR/common.json | 40 +++++++ locales/it-IT/common.json | 40 +++++++ locales/ja-JP/common.json | 40 +++++++ locales/ko-KR/common.json | 40 +++++++ locales/nl-NL/common.json | 40 +++++++ locales/pl-PL/common.json | 40 +++++++ locales/pt-BR/common.json | 40 +++++++ locales/ru-RU/common.json | 40 +++++++ locales/tr-TR/common.json | 40 +++++++ locales/vi-VN/common.json | 40 +++++++ locales/zh-CN/common.json | 40 +++++++ locales/zh-TW/common.json | 40 +++++++ next.config.mjs | 35 +++--- package.json | 13 ++- src/app/App.tsx | 21 ---- src/app/api/midjourney/route.ts | 4 +- src/app/home/Header.tsx | 25 ++++ src/app/home/index.tsx | 42 +++++++ src/app/layout.tsx | 15 ++- src/app/page.tsx | 42 +------ src/components/Analytics/Plausible.tsx | 21 ++++ src/components/Analytics/Vercel.tsx | 12 ++ src/components/Analytics/index.tsx | 19 +++ src/{app => components}/StyleRegistry.tsx | 0 src/config/client.ts | 49 ++++++++ src/config/server.ts | 20 ++++ src/const/locale.ts | 12 ++ src/const/url.ts | 3 + src/features/Input.tsx | 6 +- src/features/Preview/Guide.tsx | 11 +- .../Preview/{ => ImagePreview}/Actions.tsx | 0 .../{ => ImagePreview}/DeleteButton.tsx | 6 +- .../{ => ImagePreview}/ImagineAction.tsx | 7 +- .../MoreActions.tsx} | 8 +- .../{ => ImagePreview}/RerollButton.tsx | 4 + .../{Image.tsx => ImagePreview/index.tsx} | 4 +- src/features/Preview/Loading.tsx | 44 +++++++ src/features/Preview/index.tsx | 50 ++++---- src/features/Settings.tsx | 53 +++++---- src/hooks/useMinimode.ts | 15 ++- src/layouts/Locale.tsx | 73 ++++++++++++ src/layouts/Theme.tsx | 36 +++--- src/layouts/index.tsx | 23 ++++ src/locales/create.ts | 52 +++++++++ src/locales/resources.ts | 110 ++++++++++++++++++ src/store/global/initialState.ts | 6 +- src/store/midjourney/action.ts | 25 +++- src/store/midjourney/initialState.ts | 3 + src/store/midjourney/selectors.ts | 18 ++- src/types/locale.ts | 5 + src/utils/env.ts | 3 + src/utils/locale.ts | 10 ++ 57 files changed, 1387 insertions(+), 185 deletions(-) create mode 100644 .i18nrc.js create mode 100644 locales/ar/common.json create mode 100644 locales/de-DE/common.json create mode 100644 locales/en-US/common.json create mode 100644 locales/es-ES/common.json create mode 100644 locales/fr-FR/common.json create mode 100644 locales/it-IT/common.json create mode 100644 locales/ja-JP/common.json create mode 100644 locales/ko-KR/common.json create mode 100644 locales/nl-NL/common.json create mode 100644 locales/pl-PL/common.json create mode 100644 locales/pt-BR/common.json create mode 100644 locales/ru-RU/common.json create mode 100644 locales/tr-TR/common.json create mode 100644 locales/vi-VN/common.json create mode 100644 locales/zh-CN/common.json create mode 100644 locales/zh-TW/common.json delete mode 100644 src/app/App.tsx create mode 100644 src/app/home/Header.tsx create mode 100644 src/app/home/index.tsx create mode 100644 src/components/Analytics/Plausible.tsx create mode 100644 src/components/Analytics/Vercel.tsx create mode 100644 src/components/Analytics/index.tsx rename src/{app => components}/StyleRegistry.tsx (100%) create mode 100644 src/config/client.ts create mode 100644 src/config/server.ts create mode 100644 src/const/locale.ts create mode 100644 src/const/url.ts rename src/features/Preview/{ => ImagePreview}/Actions.tsx (100%) rename src/features/Preview/{ => ImagePreview}/DeleteButton.tsx (87%) rename src/features/Preview/{ => ImagePreview}/ImagineAction.tsx (94%) rename src/features/Preview/{ActionsBar.tsx => ImagePreview/MoreActions.tsx} (75%) rename src/features/Preview/{ => ImagePreview}/RerollButton.tsx (89%) rename src/features/Preview/{Image.tsx => ImagePreview/index.tsx} (92%) create mode 100644 src/features/Preview/Loading.tsx create mode 100644 src/layouts/Locale.tsx create mode 100644 src/layouts/index.tsx create mode 100644 src/locales/create.ts create mode 100644 src/locales/resources.ts create mode 100644 src/types/locale.ts create mode 100644 src/utils/env.ts create mode 100644 src/utils/locale.ts diff --git a/.i18nrc.js b/.i18nrc.js new file mode 100644 index 0000000..f132f01 --- /dev/null +++ b/.i18nrc.js @@ -0,0 +1,27 @@ +const { defineConfig } = require('@lobehub/i18n-cli'); + +module.exports = defineConfig({ + entry: 'locales/zh-CN', + entryLocale: 'zh-CN', + output: 'locales', + outputLocales: [ + 'ar', + 'zh-TW', + 'en-US', + 'ru-RU', + 'ja-JP', + 'ko-KR', + 'fr-FR', + 'tr-TR', + 'es-ES', + 'pt-BR', + 'de-DE', + 'it-IT', + 'nl-NL', + 'pl-PL', + 'vi-VN', + ], + temperature: 0, + modelName: 'gpt-3.5-turbo-1106', + splitToken: 1024, +}); diff --git a/locales/ar/common.json b/locales/ar/common.json new file mode 100644 index 0000000..896a6e8 --- /dev/null +++ b/locales/ar/common.json @@ -0,0 +1,40 @@ +{ + "appIniting": "جار تشغيل التطبيق...", + "guide": { + "step1": { + "description": "انتقل إلى الإعدادات واملأ عنوان وكيل Midjourney API", + "title": "ربط خدمة Midjourney API" + }, + "step2": { + "description": "أدخل كلمات الإرشاد في الحقل وانقر على زر الإنشاء لبدء الإنشاء", + "title": "بدء الرسم" + } + }, + "images": { + "delete": "حذف", + "deleteConfirm": "هل أنت متأكد أنك تريد حذف هذه الصورة؟" + }, + "input": { + "placeholder": "الرجاء إدخال كلمات الإرشاد لـ Midjourney..." + }, + "requestError": "فشل الطلب، رمز الخطأ {{errorCode}}", + "response": { + "NO_BASE_URL": "عنوان وكيل Midjourney API فارغ، يرجى ملء العنوان والمحاولة مرة أخرى", + "fallback": "فشل الطلب، يرجى المحاولة مرة أخرى في وقت لاحق" + }, + "settings": { + "MidjourneyAPIProxy": { + "description": "يرجى الرجوع إلى <1>midjourney-proxy ونشر الخدمة الخادمية لاستخدامها", + "title": "عنوان وكيل Midjourney API" + }, + "modalTitle": "الإعدادات", + "save": "حفظ" + }, + "task": { + "actions": { + "reroll": "إعادة الإنشاء", + "upscale": "تحسين الجودة", + "variant": "تنويع" + } + } +} diff --git a/locales/de-DE/common.json b/locales/de-DE/common.json new file mode 100644 index 0000000..e336d76 --- /dev/null +++ b/locales/de-DE/common.json @@ -0,0 +1,40 @@ +{ + "appIniting": "Anwendung wird gestartet...", + "guide": { + "step1": { + "description": "Gehen Sie zu den Einstellungen und geben Sie die Adresse des Midjourney API-Proxys ein", + "title": "Midjourney API-Dienst verbinden" + }, + "step2": { + "description": "Geben Sie einen Hinweis in das Eingabefeld ein und klicken Sie auf die Schaltfläche Generieren, um zu beginnen", + "title": "Kartierung starten" + } + }, + "images": { + "delete": "Löschen", + "deleteConfirm": "Möchten Sie dieses Bild wirklich löschen?" + }, + "input": { + "placeholder": "Geben Sie einen Midjourney-Hinweis ein..." + }, + "requestError": "Anforderung fehlgeschlagen, Fehlercode {{errorCode}}", + "response": { + "NO_BASE_URL": "Die Adresse des Midjourney API-Proxys ist leer. Bitte füllen Sie sie aus und versuchen Sie es erneut", + "fallback": "Anfrage fehlgeschlagen. Bitte versuchen Sie es später erneut" + }, + "settings": { + "MidjourneyAPIProxy": { + "description": "Bitte installieren Sie den <1>midjourney-proxy-Server und verwenden Sie ihn", + "title": "Midjourney API-Proxy-Adresse" + }, + "modalTitle": "Einstellungen", + "save": "Speichern" + }, + "task": { + "actions": { + "reroll": "Neu generieren", + "upscale": "Hochskalieren", + "variant": "Varianten erstellen" + } + } +} diff --git a/locales/en-US/common.json b/locales/en-US/common.json new file mode 100644 index 0000000..ea59158 --- /dev/null +++ b/locales/en-US/common.json @@ -0,0 +1,40 @@ +{ + "appIniting": "App is initializing...", + "guide": { + "step1": { + "description": "Go to settings and fill in the Midjourney API proxy address", + "title": "Bind Midjourney API Service" + }, + "step2": { + "description": "Enter the prompt word in the input box and click the generate button to start generating", + "title": "Start Mapping" + } + }, + "images": { + "delete": "Delete", + "deleteConfirm": "Are you sure you want to delete this image?" + }, + "input": { + "placeholder": "Enter Midjourney prompt word..." + }, + "requestError": "Request failed, error code {{errorCode}}", + "response": { + "NO_BASE_URL": "Midjourney API proxy address is empty, please fill it in and try again", + "fallback": "Request failed, please try again later" + }, + "settings": { + "MidjourneyAPIProxy": { + "description": "Please refer to <1>midjourney-proxy for deploying the server and then use", + "title": "Midjourney API Proxy Address" + }, + "modalTitle": "Settings", + "save": "Save" + }, + "task": { + "actions": { + "reroll": "Reroll", + "upscale": "Upscale", + "variant": "Diversify" + } + } +} diff --git a/locales/es-ES/common.json b/locales/es-ES/common.json new file mode 100644 index 0000000..83da044 --- /dev/null +++ b/locales/es-ES/common.json @@ -0,0 +1,40 @@ +{ + "appIniting": "Iniciando la aplicación...", + "guide": { + "step1": { + "description": "Ir a configuración y completar la dirección del proxy de la API de Midjourney", + "title": "Vincular el servicio de API de Midjourney" + }, + "step2": { + "description": "Ingresar palabras clave en el campo de entrada y hacer clic en el botón de generación para comenzar", + "title": "Comenzar a generar imágenes" + } + }, + "images": { + "delete": "Eliminar", + "deleteConfirm": "¿Estás seguro de que quieres eliminar esta imagen?" + }, + "input": { + "placeholder": "Ingrese las palabras clave de Midjourney..." + }, + "requestError": "Error en la solicitud, código de error {{errorCode}}", + "response": { + "NO_BASE_URL": "La dirección del proxy de la API de Midjourney está vacía, por favor completa y vuelve a intentarlo", + "fallback": "Error en la solicitud, por favor inténtalo de nuevo más tarde" + }, + "settings": { + "MidjourneyAPIProxy": { + "description": "Por favor consulta <1>midjourney-proxy para desplegar el servidor y luego utilizarlo", + "title": "Dirección del proxy de la API de Midjourney" + }, + "modalTitle": "Configuración", + "save": "Guardar" + }, + "task": { + "actions": { + "reroll": "Volver a generar", + "upscale": "Mejorar la calidad", + "variant": "Variar" + } + } +} diff --git a/locales/fr-FR/common.json b/locales/fr-FR/common.json new file mode 100644 index 0000000..7f86d8e --- /dev/null +++ b/locales/fr-FR/common.json @@ -0,0 +1,40 @@ +{ + "appIniting": "L'application démarre...", + "guide": { + "step1": { + "description": "Accédez aux paramètres et saisissez l'adresse du proxy Midjourney API", + "title": "Associer le service Midjourney API" + }, + "step2": { + "description": "Saisissez un mot-clé dans le champ de saisie, puis cliquez sur le bouton de génération", + "title": "Commencer la génération d'images" + } + }, + "images": { + "delete": "Supprimer", + "deleteConfirm": "Êtes-vous sûr de vouloir supprimer cette image ?" + }, + "input": { + "placeholder": "Veuillez saisir un mot-clé Midjourney..." + }, + "requestError": "Échec de la requête, code d'erreur {{errorCode}}", + "response": { + "NO_BASE_URL": "L'adresse du proxy Midjourney API est vide, veuillez la remplir et réessayer", + "fallback": "Échec de la requête, veuillez réessayer ultérieurement" + }, + "settings": { + "MidjourneyAPIProxy": { + "description": "Veuillez consulter <1>midjourney-proxy pour déployer le service côté serveur avant utilisation", + "title": "Adresse du proxy Midjourney API" + }, + "modalTitle": "Paramètres", + "save": "Enregistrer" + }, + "task": { + "actions": { + "reroll": "Nouvelle génération", + "upscale": "Améliorer la qualité", + "variant": "Diversifier" + } + } +} diff --git a/locales/it-IT/common.json b/locales/it-IT/common.json new file mode 100644 index 0000000..82d631a --- /dev/null +++ b/locales/it-IT/common.json @@ -0,0 +1,40 @@ +{ + "appIniting": "Avvio dell'applicazione...", + "guide": { + "step1": { + "description": "Vai alle impostazioni e inserisci l'indirizzo del proxy API di Midjourney", + "title": "Collega il servizio API di Midjourney" + }, + "step2": { + "description": "Inserisci le parole chiave nel campo di input e clicca sul pulsante di generazione", + "title": "Inizia a generare l'immagine" + } + }, + "images": { + "delete": "Elimina", + "deleteConfirm": "Sei sicuro di voler eliminare questa immagine?" + }, + "input": { + "placeholder": "Inserisci le parole chiave di Midjourney..." + }, + "requestError": "Richiesta fallita, codice di errore {{errorCode}}", + "response": { + "NO_BASE_URL": "L'indirizzo del proxy API di Midjourney è vuoto, si prega di compilare e riprovare", + "fallback": "Richiesta fallita, si prega di riprovare più tardi" + }, + "settings": { + "MidjourneyAPIProxy": { + "description": "Si prega di fare riferimento a <1>midjourney-proxy per utilizzare il servizio lato server", + "title": "Indirizzo del proxy API di Midjourney" + }, + "modalTitle": "Impostazioni", + "save": "Salva" + }, + "task": { + "actions": { + "reroll": "Rigenera", + "upscale": "Aumenta la risoluzione", + "variant": "Variazione" + } + } +} diff --git a/locales/ja-JP/common.json b/locales/ja-JP/common.json new file mode 100644 index 0000000..ad8673a --- /dev/null +++ b/locales/ja-JP/common.json @@ -0,0 +1,40 @@ +{ + "appIniting": "アプリを起動しています...", + "guide": { + "step1": { + "description": "設定に入り、Midjourney APIプロキシアドレスを入力してください", + "title": "Midjourney APIサービスをバインド" + }, + "step2": { + "description": "入力ボックスにヒントワードを入力し、生成ボタンをクリックして生成を開始します", + "title": "画像生成を開始" + } + }, + "images": { + "delete": "削除", + "deleteConfirm": "この画像を削除してもよろしいですか?" + }, + "input": { + "placeholder": "Midjourneyのヒントワードを入力してください..." + }, + "requestError": "リクエストが失敗しました、エラーコード {{errorCode}}", + "response": { + "NO_BASE_URL": "Midjourney APIプロキシアドレスが空です。入力してから再試行してください", + "fallback": "リクエストが失敗しました。しばらくしてから再試行してください" + }, + "settings": { + "MidjourneyAPIProxy": { + "description": "サーバーをデプロイした後、<1>midjourney-proxy を参照して使用してください", + "title": "Midjourney APIプロキシアドレス" + }, + "modalTitle": "設定", + "save": "保存" + }, + "task": { + "actions": { + "reroll": "再生成", + "upscale": "高画質化", + "variant": "多様化" + } + } +} diff --git a/locales/ko-KR/common.json b/locales/ko-KR/common.json new file mode 100644 index 0000000..cc356a0 --- /dev/null +++ b/locales/ko-KR/common.json @@ -0,0 +1,40 @@ +{ + "appIniting": "앱 시작 중...", + "guide": { + "step1": { + "description": "설정으로 이동하여 Midjourney API 프록시 주소를 입력하세요", + "title": "Midjourney API 서비스 바인딩" + }, + "step2": { + "description": "입력란에 힌트 단어를 입력하고 생성 버튼을 클릭하여 생성하세요", + "title": "그림 생성 시작" + } + }, + "images": { + "delete": "삭제", + "deleteConfirm": "이 그림을 삭제하시겠습니까?" + }, + "input": { + "placeholder": "Midjourney 힌트 단어를 입력하세요..." + }, + "requestError": "요청 실패, 오류 코드 {{errorCode}}", + "response": { + "NO_BASE_URL": "Midjourney API 프록시 주소가 비어 있습니다. 입력 후 다시 시도하세요", + "fallback": "요청 실패, 잠시 후 다시 시도하세요" + }, + "settings": { + "MidjourneyAPIProxy": { + "description": "<1>midjourney-proxy를 참조하여 서버를 배포한 후 사용하세요", + "title": "Midjourney API 프록시 주소" + }, + "modalTitle": "설정", + "save": "저장" + }, + "task": { + "actions": { + "reroll": "다시 생성", + "upscale": "고화질화", + "variant": "다양화" + } + } +} diff --git a/locales/nl-NL/common.json b/locales/nl-NL/common.json new file mode 100644 index 0000000..d619bb9 --- /dev/null +++ b/locales/nl-NL/common.json @@ -0,0 +1,40 @@ +{ + "appIniting": "Applicatie wordt gestart...", + "guide": { + "step1": { + "description": "Ga naar instellingen en vul het adres van de Midjourney API-proxy in", + "title": "Bind Midjourney API-service" + }, + "step2": { + "description": "Voer een suggestiewoord in het invoerveld in en klik op de knop Genereren om te starten", + "title": "Begin met genereren van afbeeldingen" + } + }, + "images": { + "delete": "Verwijderen", + "deleteConfirm": "Weet je zeker dat je deze afbeelding wilt verwijderen?" + }, + "input": { + "placeholder": "Voer een suggestiewoord voor Midjourney in..." + }, + "requestError": "Verzoek mislukt, foutcode {{errorCode}}", + "response": { + "NO_BASE_URL": "Het adres van de Midjourney API-proxy is leeg, vul het in en probeer het opnieuw", + "fallback": "Verzoek mislukt, probeer het later opnieuw" + }, + "settings": { + "MidjourneyAPIProxy": { + "description": "Raadpleeg <1>midjourney-proxy voor het implementeren van de serverzijde service", + "title": "Adres van de Midjourney API-proxy" + }, + "modalTitle": "Instellingen", + "save": "Opslaan" + }, + "task": { + "actions": { + "reroll": "Opnieuw genereren", + "upscale": "Opschalen", + "variant": "Diversifiëren" + } + } +} diff --git a/locales/pl-PL/common.json b/locales/pl-PL/common.json new file mode 100644 index 0000000..09b7ea0 --- /dev/null +++ b/locales/pl-PL/common.json @@ -0,0 +1,40 @@ +{ + "appIniting": "Uruchamianie aplikacji...", + "guide": { + "step1": { + "description": "Przejdź do ustawień i wprowadź adres proxy API Midjourney", + "title": "Powiąż usługę API Midjourney" + }, + "step2": { + "description": "Wprowadź sugestię w polu tekstowym i kliknij przycisk generowania", + "title": "Rozpocznij generowanie obrazu" + } + }, + "images": { + "delete": "Usuń", + "deleteConfirm": "Czy na pewno chcesz usunąć ten obraz?" + }, + "input": { + "placeholder": "Wprowadź sugestię dla Midjourney..." + }, + "requestError": "Błąd żądania, kod błędu {{errorCode}}", + "response": { + "NO_BASE_URL": "Adres proxy API Midjourney jest pusty. Proszę wypełnić i spróbować ponownie", + "fallback": "Żądanie nie powiodło się. Proszę spróbować ponownie później" + }, + "settings": { + "MidjourneyAPIProxy": { + "description": "Proszę skonsultować się z <1>midjourney-proxy w celu skonfigurowania serwera", + "title": "Adres proxy API Midjourney" + }, + "modalTitle": "Ustawienia", + "save": "Zapisz" + }, + "task": { + "actions": { + "reroll": "Generuj ponownie", + "upscale": "Zwiększ jakość", + "variant": "Warianty" + } + } +} diff --git a/locales/pt-BR/common.json b/locales/pt-BR/common.json new file mode 100644 index 0000000..015bf52 --- /dev/null +++ b/locales/pt-BR/common.json @@ -0,0 +1,40 @@ +{ + "appIniting": "Iniciando o aplicativo...", + "guide": { + "step1": { + "description": "Acesse as configurações e insira o endereço do proxy da API Midjourney", + "title": "Vincular o serviço Midjourney API" + }, + "step2": { + "description": "Digite palavras-chave na caixa de entrada e clique no botão de geração para começar", + "title": "Começar a criar imagens" + } + }, + "images": { + "delete": "Excluir", + "deleteConfirm": "Tem certeza de que deseja excluir esta imagem?" + }, + "input": { + "placeholder": "Digite as palavras-chave do Midjourney..." + }, + "requestError": "Falha na solicitação, código de erro {{errorCode}}", + "response": { + "NO_BASE_URL": "O endereço do proxy da API Midjourney está vazio, por favor preencha e tente novamente", + "fallback": "Falha na solicitação, por favor tente novamente mais tarde" + }, + "settings": { + "MidjourneyAPIProxy": { + "description": "Consulte <1>midjourney-proxy para implantar o servidor e usá-lo", + "title": "Endereço do proxy da API Midjourney" + }, + "modalTitle": "Configurações", + "save": "Salvar" + }, + "task": { + "actions": { + "reroll": "Rolar novamente", + "upscale": "Aprimorar", + "variant": "Variar" + } + } +} diff --git a/locales/ru-RU/common.json b/locales/ru-RU/common.json new file mode 100644 index 0000000..9fc9e4d --- /dev/null +++ b/locales/ru-RU/common.json @@ -0,0 +1,40 @@ +{ + "appIniting": "Приложение запускается...", + "guide": { + "step1": { + "description": "Зайдите в настройки и введите адрес прокси Midjourney API", + "title": "Привязка сервиса Midjourney API" + }, + "step2": { + "description": "Введите подсказку в поле ввода и нажмите кнопку создать для начала процесса", + "title": "Начать создание изображения" + } + }, + "images": { + "delete": "Удалить", + "deleteConfirm": "Вы уверены, что хотите удалить это изображение?" + }, + "input": { + "placeholder": "Введите подсказку для Midjourney..." + }, + "requestError": "Ошибка запроса, код ошибки {{errorCode}}", + "response": { + "NO_BASE_URL": "Адрес прокси Midjourney API не указан, пожалуйста, укажите его и повторите попытку", + "fallback": "Ошибка запроса, пожалуйста, повторите попытку позже" + }, + "settings": { + "MidjourneyAPIProxy": { + "description": "Пожалуйста, укажите адрес сервера для использования <1>midjourney-proxy", + "title": "Адрес прокси Midjourney API" + }, + "modalTitle": "Настройки", + "save": "Сохранить" + }, + "task": { + "actions": { + "reroll": "Перегенерировать", + "upscale": "Увеличить размер", + "variant": "Варианты" + } + } +} diff --git a/locales/tr-TR/common.json b/locales/tr-TR/common.json new file mode 100644 index 0000000..fb3e455 --- /dev/null +++ b/locales/tr-TR/common.json @@ -0,0 +1,40 @@ +{ + "appIniting": "Uygulama başlatılıyor...", + "guide": { + "step1": { + "description": "Ayarlar'a gidin ve Midjourney API vekil adresini girin", + "title": "Midjourney API Servisine Bağlan" + }, + "step2": { + "description": "Giriş kutusuna ipucu kelime girin ve oluştur'a tıklayarak oluşturmaya başlayın", + "title": "Harita Oluşturmaya Başla" + } + }, + "images": { + "delete": "Sil", + "deleteConfirm": "Bu resmi silmek istediğinize emin misiniz?" + }, + "input": { + "placeholder": "Midjourney ipucu kelimesini girin..." + }, + "requestError": "İstek başarısız oldu, hata kodu {{errorCode}}", + "response": { + "NO_BASE_URL": "Midjourney API vekil adresi boş, lütfen doldurup tekrar deneyin", + "fallback": "İstek başarısız, lütfen sonra tekrar deneyin" + }, + "settings": { + "MidjourneyAPIProxy": { + "description": "Lütfen <1>midjourney-proxy bağlantısına giderek sunucu tarafını kurduktan sonra kullanın", + "title": "Midjourney API Vekil Adresi" + }, + "modalTitle": "Ayarlar", + "save": "Kaydet" + }, + "task": { + "actions": { + "reroll": "Yeniden Oluştur", + "upscale": "Yüksek Kalite", + "variant": "Çeşitlendir" + } + } +} diff --git a/locales/vi-VN/common.json b/locales/vi-VN/common.json new file mode 100644 index 0000000..b04fa30 --- /dev/null +++ b/locales/vi-VN/common.json @@ -0,0 +1,40 @@ +{ + "appIniting": "Ứng dụng đang khởi động...", + "guide": { + "step1": { + "description": "Điều hướng đến cài đặt, điền địa chỉ proxy API Midjourney", + "title": "Liên kết dịch vụ API Midjourney" + }, + "step2": { + "description": "Nhập từ khóa gợi ý vào ô nhập và nhấn nút tạo để bắt đầu tạo", + "title": "Bắt đầu tạo hình ảnh" + } + }, + "images": { + "delete": "Xóa", + "deleteConfirm": "Bạn có chắc muốn xóa hình ảnh này không?" + }, + "input": { + "placeholder": "Vui lòng nhập từ khóa Midjourney..." + }, + "requestError": "Yêu cầu thất bại, mã lỗi {{errorCode}}", + "response": { + "NO_BASE_URL": "Địa chỉ proxy API Midjourney trống, vui lòng điền và thử lại", + "fallback": "Yêu cầu thất bại, vui lòng thử lại sau" + }, + "settings": { + "MidjourneyAPIProxy": { + "description": "Vui lòng tham khảo <1>midjourney-proxy để triển khai dịch vụ máy chủ trước khi sử dụng", + "title": "Địa chỉ proxy API Midjourney" + }, + "modalTitle": "Cài đặt", + "save": "Lưu" + }, + "task": { + "actions": { + "reroll": "Tạo lại", + "upscale": "Nâng cấp chất lượng", + "variant": "Đa dạng hóa" + } + } +} diff --git a/locales/zh-CN/common.json b/locales/zh-CN/common.json new file mode 100644 index 0000000..7a995eb --- /dev/null +++ b/locales/zh-CN/common.json @@ -0,0 +1,40 @@ +{ + "appIniting": "应用启动中...", + "guide": { + "step1": { + "title": "绑定 Midjourney API 服务", + "description": "进入设置,填写 Midjourney API 代理地址" + }, + "step2": { + "title": "开始出图", + "description": "输入框中输入提示词,点击生成按钮开始生成" + } + }, + "images": { + "delete": "删除", + "deleteConfirm": "确定要删除这张图片吗?" + }, + "input": { + "placeholder": "请输入 Midjourney 提示词..." + }, + "requestError": "`请求失败,错误码 {{errorCode}}", + "response": { + "NO_BASE_URL": "Midjourney API 代理地址为空,请填写后重试", + "fallback": "请求失败,请稍后重试" + }, + "settings": { + "MidjourneyAPIProxy": { + "title": "Midjourney API 代理地址", + "description": "请参考 <1>midjourney-proxy 部署好服务端后使用" + }, + "modalTitle": "设置", + "save": "保存" + }, + "task": { + "actions": { + "reroll": "重新生成", + "upscale": "高清化", + "variant": "多样化" + } + } +} diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json new file mode 100644 index 0000000..eca9a48 --- /dev/null +++ b/locales/zh-TW/common.json @@ -0,0 +1,40 @@ +{ + "appIniting": "應用程式啟動中...", + "guide": { + "step1": { + "description": "進入設定,填寫 Midjourney API 代理地址", + "title": "綁定 Midjourney API 服務" + }, + "step2": { + "description": "輸入框中輸入提示詞,點擊生成按鈕開始生成", + "title": "開始出圖" + } + }, + "images": { + "delete": "刪除", + "deleteConfirm": "確定要刪除這張圖片嗎?" + }, + "input": { + "placeholder": "請輸入 Midjourney 提示詞..." + }, + "requestError": "請求失敗,錯誤碼 {{errorCode}}", + "response": { + "NO_BASE_URL": "Midjourney API 代理地址為空,請填寫後重試", + "fallback": "請求失敗,請稍後重試" + }, + "settings": { + "MidjourneyAPIProxy": { + "description": "請參考 <1>midjourney-proxy 部署好服務端後使用", + "title": "Midjourney API 代理地址" + }, + "modalTitle": "設定", + "save": "保存" + }, + "task": { + "actions": { + "reroll": "重新生成", + "upscale": "高清化", + "variant": "多樣化" + } + } +} diff --git a/next.config.mjs b/next.config.mjs index a58e67a..70109de 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -3,7 +3,10 @@ const isProd = process.env.NODE_ENV === 'production'; /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, - transpilePackages: ['@lobehub/ui'], + experimental: { + optimizePackageImports: ['@icons-pack/react-simple-icons', '@lobehub/ui'], + webVitalsAttribution: ['CLS', 'LCP'], + }, images: { remotePatterns: [ { @@ -15,23 +18,25 @@ const nextConfig = { ], unoptimized: !isProd, }, + async headers() { - return [ - { - // matching all API routes - source: '/:path*', - headers: [ - { key: 'Access-Control-Allow-Credentials', value: 'true' }, - { key: 'Access-Control-Allow-Origin', value: '*' }, - { key: 'Access-Control-Allow-Methods', value: 'GET,OPTIONS,PATCH,DELETE,POST,PUT' }, + return isProd + ? undefined + : [ { - key: 'Access-Control-Allow-Headers', - value: - 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version', + source: '/:path*', + headers: [ + { key: 'Access-Control-Allow-Credentials', value: 'true' }, + { key: 'Access-Control-Allow-Origin', value: '*' }, + { key: 'Access-Control-Allow-Methods', value: 'GET,OPTIONS,PATCH,DELETE,POST,PUT' }, + { + key: 'Access-Control-Allow-Headers', + value: + 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version', + }, + ], }, - ], - }, - ]; + ]; }, }; diff --git a/package.json b/package.json index 9e5440f..c4f5d85 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "scripts": { "build": "next build", "ci": "npm run lint && npm run type-check", - "dev": "next dev -p 3400", + "dev": "next dev -p 3040", + "i18n": "lobe-i18n", "lint": "eslint \"{src,api,docs}/**/*.{js,jsx,ts,tsx}\" --fix", "lint:md": "remark . --quiet --frail --output", "lint:style": "stylelint \"src/**/*.{js,jsx,ts,tsx}\" --fix", @@ -48,21 +49,28 @@ "dependencies": { "@lobehub/chat-plugin-sdk": "^1", "@lobehub/chat-plugins-gateway": "^1", + "@lobehub/i18n-cli": "^1.15.3", "@lobehub/ui": "^1", + "@vercel/analytics": "^1.1.2", "ahooks": "^3.7.8", "antd": "^5", "antd-style": "^3", "dayjs": "^1", "fast-deep-equal": "^3.1.3", + "i18next": "^23.7.16", + "i18next-browser-languagedetector": "^7.2.0", + "i18next-resources-to-backend": "^1.2.0", "immer": "^10.0.3", - "lucide-react": "^0.309.0", + "lucide-react": "latest", "next": "14.0.4", "polished": "^4.2.2", "qs": "^6.11.2", "react": "^18", "react-dom": "^18", + "react-i18next": "^14.0.0", "react-image-size": "^2.3.2", "react-layout-kit": "^1", + "rtl-detect": "^1.1.2", "swr": "^2.2.4", "url-join": "^5.0.0", "zustand": "^4.4.7" @@ -73,6 +81,7 @@ "@next/eslint-plugin-next": "14.0.4", "@types/qs": "^6.9.11", "@types/react": "18", + "@types/rtl-detect": "^1.0.3", "@vercel/node": "^3", "@vitest/coverage-v8": "^1", "commitlint": "^18", diff --git a/src/app/App.tsx b/src/app/App.tsx deleted file mode 100644 index fcc7c6e..0000000 --- a/src/app/App.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { CSSProperties, memo } from 'react'; -import { Flexbox } from 'react-layout-kit'; -import ImagePreview from 'src/features/Preview'; - -import PromptInput from '@/features/Input'; -import TaskList from '@/features/TaskList'; -import { useUpdateView } from '@/hooks/useUpdateView'; - -interface AppProps { - style?: CSSProperties; -} -export const App = memo(({ style }) => { - useUpdateView(); - return ( - - - - - - ); -}); diff --git a/src/app/api/midjourney/route.ts b/src/app/api/midjourney/route.ts index 09e4853..3ca1c2f 100644 --- a/src/app/api/midjourney/route.ts +++ b/src/app/api/midjourney/route.ts @@ -1,8 +1,10 @@ import urlJoin from 'url-join'; +import { getServerConfig } from '@/config/server'; + export const runtime = 'edge'; -const base = process.env.MIDJOURNEY_PROXY_URL; +const { MIDJOURNEY_PROXY_URL: base } = getServerConfig(); const getBase = (req: Request) => { const userDefineBaseUrl = req.headers.get('X-Midjourney-Proxy-Url'); diff --git a/src/app/home/Header.tsx b/src/app/home/Header.tsx new file mode 100644 index 0000000..c45e437 --- /dev/null +++ b/src/app/home/Header.tsx @@ -0,0 +1,25 @@ +import { ActionIcon, DiscordIcon, Logo } from '@lobehub/ui'; +import { LucideGithub } from 'lucide-react'; +import Link from 'next/link'; +import { memo } from 'react'; +import { Flexbox } from 'react-layout-kit'; + +import { GITHUB_REPO } from '@/const/url'; +import Settings from '@/features/Settings'; + +const Header = memo(() => ( + + + + + + + + + + + + +)); + +export default Header; diff --git a/src/app/home/index.tsx b/src/app/home/index.tsx new file mode 100644 index 0000000..a55855a --- /dev/null +++ b/src/app/home/index.tsx @@ -0,0 +1,42 @@ +import { useTheme } from 'antd-style'; +import { memo } from 'react'; +import { Flexbox } from 'react-layout-kit'; + +import PromptInput from '@/features/Input'; +import ImagePreview from '@/features/Preview'; +import TaskList from '@/features/TaskList'; +import { useMidjourneyStore } from '@/store/midjourney'; + +import Header from './Header'; + +const Home = memo(() => { + const theme = useTheme(); + + const [useInitApp] = useMidjourneyStore((s) => [s.useInitApp]); + + useInitApp(); + + return ( + + +
+ + + + + + + + ); +}); + +export default Home; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f1ea266..baa90bc 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,21 +1,26 @@ import { cookies } from 'next/headers'; import { PropsWithChildren } from 'react'; +import { isRtlLang } from 'rtl-detect'; +import StyleRegistry from '@/components/StyleRegistry'; +import { DEFAULT_LANG, LOBE_LOCALE_COOKIE } from '@/const/locale'; import { LOBE_THEME_APPEARANCE } from '@/const/theme'; -import Layout from '@/layouts/Theme'; - -import StyleRegistry from './StyleRegistry'; +import Layout from '@/layouts'; const RootLayout = ({ children }: PropsWithChildren) => { // get default theme config to use with ssr const cookieStore = cookies(); const appearance = cookieStore.get(LOBE_THEME_APPEARANCE); + const lang = cookieStore.get(LOBE_LOCALE_COOKIE); + const direction = isRtlLang(lang?.value || DEFAULT_LANG) ? 'rtl' : 'ltr'; return ( - + - {children} + + {children} + diff --git a/src/app/page.tsx b/src/app/page.tsx index 8897638..15d4713 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,43 +1,3 @@ 'use client'; -import { Logo } from '@lobehub/ui'; -import { useTheme } from 'antd-style'; -import { memo } from 'react'; -import { Flexbox } from 'react-layout-kit'; - -import Settings from '@/features/Settings'; -import { useMidjourneyStore } from '@/store/midjourney'; - -import { App } from './App'; - -const Page = memo(() => { - const [useInitApp] = useMidjourneyStore((s) => [s.useInitApp]); - - useInitApp(); - - const theme = useTheme(); - - return ( - - - { - window.open('https://github.com/lobehub/chat-plugin-midjourney', '_blank'); - }} - style={{ cursor: 'pointer' }} - type={'combine'} - /> - - - - - ); -}); - -export default Page; +export { default } from './home'; diff --git a/src/components/Analytics/Plausible.tsx b/src/components/Analytics/Plausible.tsx new file mode 100644 index 0000000..49cb600 --- /dev/null +++ b/src/components/Analytics/Plausible.tsx @@ -0,0 +1,21 @@ +'use client'; + +import Script from 'next/script'; +import { memo } from 'react'; + +import { getClientConfig } from '@/config/client'; + +const { PLAUSIBLE_DOMAIN, PLAUSIBLE_SCRIPT_BASE_URL } = getClientConfig(); + +const PlausibleAnalytics = memo( + () => + PLAUSIBLE_DOMAIN && ( +