diff --git a/apps/image/package.json b/apps/image/package.json index 39b39e3f..9cf2ca44 100644 --- a/apps/image/package.json +++ b/apps/image/package.json @@ -29,9 +29,11 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.23.24", + "i18next": "^26.0.6", "lucide-react": "^0.555.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-i18next": "^17.0.4", "tailwind-merge": "^3.4.0", "uuid": "^13.0.0", "zustand": "^4.5.2" diff --git a/apps/image/src/components/editor/SettingsDialog.tsx b/apps/image/src/components/editor/SettingsDialog.tsx index e60e64b2..ac2ade10 100644 --- a/apps/image/src/components/editor/SettingsDialog.tsx +++ b/apps/image/src/components/editor/SettingsDialog.tsx @@ -1,5 +1,6 @@ -import { useState } from 'react'; +import { useState, type ReactNode } from 'react'; import { X, Settings, Grid3X3, MousePointer, Save, Palette, Monitor } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; import { useUIStore } from '../../stores/ui-store'; import { Slider } from '@openreel/ui'; @@ -12,6 +13,7 @@ type SettingsTab = 'canvas' | 'snapping' | 'appearance'; export function SettingsDialog({ isOpen, onClose }: Props) { const [activeTab, setActiveTab] = useState('canvas'); + const { t } = useTranslation(); const { showGrid, @@ -32,10 +34,10 @@ export function SettingsDialog({ isOpen, onClose }: Props) { if (!isOpen) return null; - const tabs: { id: SettingsTab; label: string; icon: React.ReactNode }[] = [ - { id: 'canvas', label: 'Canvas', icon: }, - { id: 'snapping', label: 'Snapping', icon: }, - { id: 'appearance', label: 'Appearance', icon: }, + const tabs: { id: SettingsTab; label: string; icon: ReactNode }[] = [ + { id: 'canvas', label: t('settings:tabs.canvas'), icon: }, + { id: 'snapping', label: t('settings:tabs.snapping'), icon: }, + { id: 'appearance', label: t('settings:tabs.appearance'), icon: }, ]; return ( @@ -45,7 +47,7 @@ export function SettingsDialog({ isOpen, onClose }: Props) {
-

Settings

+

{t('settings:dialog.title')}

@@ -161,7 +163,7 @@ export function TransformSection({ layer }: Props) { ? 'bg-primary/20 border-primary text-primary' : 'bg-secondary/50 border-border text-muted-foreground hover:text-foreground hover:bg-secondary' }`} - title="Flip Vertical" + title={t('inspector:transform.flipVertical')} > @@ -169,7 +171,7 @@ export function TransformSection({ layer }: Props) { @@ -177,7 +179,7 @@ export function TransformSection({ layer }: Props) { diff --git a/apps/image/src/i18n.ts b/apps/image/src/i18n.ts new file mode 100644 index 00000000..9524ec04 --- /dev/null +++ b/apps/image/src/i18n.ts @@ -0,0 +1,30 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import common from "./locales/en/common.json"; +import editor from "./locales/en/editor.json"; +import inspector from "./locales/en/inspector.json"; +import settings from "./locales/en/settings.json"; + +const resources = { + en: { + common, + editor, + inspector, + settings, + }, +} as const; + +if (!i18n.isInitialized) { + void i18n.use(initReactI18next).init({ + resources, + lng: "en", + fallbackLng: "en", + ns: ["common", "editor", "inspector", "settings"], + defaultNS: "common", + interpolation: { + escapeValue: false, + }, + }); +} + +export default i18n; diff --git a/apps/image/src/locales/en/common.json b/apps/image/src/locales/en/common.json new file mode 100644 index 00000000..63f86978 --- /dev/null +++ b/apps/image/src/locales/en/common.json @@ -0,0 +1,5 @@ +{ + "buttons": { + "close": "Close" + } +} diff --git a/apps/image/src/locales/en/editor.json b/apps/image/src/locales/en/editor.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/apps/image/src/locales/en/editor.json @@ -0,0 +1 @@ +{} diff --git a/apps/image/src/locales/en/inspector.json b/apps/image/src/locales/en/inspector.json new file mode 100644 index 00000000..fbff6ecd --- /dev/null +++ b/apps/image/src/locales/en/inspector.json @@ -0,0 +1,17 @@ +{ + "transform": { + "title": "Transform", + "x": "X", + "y": "Y", + "width": "Width", + "height": "Height", + "rotation": "Rotation", + "opacity": "Opacity", + "skewX": "Skew X", + "skewY": "Skew Y", + "flipHorizontal": "Flip Horizontal", + "flipVertical": "Flip Vertical", + "rotateCounterClockwise": "Rotate 90 degrees Counter-clockwise", + "rotateClockwise": "Rotate 90 degrees Clockwise" + } +} diff --git a/apps/image/src/locales/en/settings.json b/apps/image/src/locales/en/settings.json new file mode 100644 index 00000000..235e1f70 --- /dev/null +++ b/apps/image/src/locales/en/settings.json @@ -0,0 +1,38 @@ +{ + "dialog": { + "title": "Settings" + }, + "tabs": { + "canvas": "Canvas", + "snapping": "Snapping", + "appearance": "Appearance" + }, + "canvas": { + "title": "Canvas Options", + "showGrid": "Show Grid", + "showGridDescription": "Display grid overlay on canvas", + "showGuides": "Show Guides", + "showGuidesDescription": "Display alignment guides", + "showRulers": "Show Rulers", + "showRulersDescription": "Display rulers on edges", + "gridSize": "Grid Size" + }, + "snapping": { + "title": "Snap Options", + "snapToGrid": "Snap to Grid", + "snapToGridDescription": "Snap objects to grid intersections", + "snapToGuides": "Snap to Guides", + "snapToGuidesDescription": "Snap objects to guide lines", + "snapToObjects": "Snap to Objects", + "snapToObjectsDescription": "Snap objects to other objects" + }, + "appearance": { + "title": "Appearance", + "theme": "Theme", + "themeDescription": "Interface appearance", + "themeValue": "Dark (System)", + "autoSave": "Auto Save", + "autoSaveDescription": "Automatically save projects", + "autoSaveSummary": "Projects are automatically saved to browser storage every 30 seconds." + } +} diff --git a/apps/image/src/main.tsx b/apps/image/src/main.tsx index 2339d59c..f8d90260 100644 --- a/apps/image/src/main.tsx +++ b/apps/image/src/main.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; +import './i18n'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/apps/web/package.json b/apps/web/package.json index 81bd4da7..2090b3fb 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -33,10 +33,12 @@ "clsx": "^2.1.1", "framer-motion": "^12.23.24", "gsap": "^3.14.2", + "i18next": "^26.0.6", "lucide-react": "^0.555.0", "posthog-js": "^1.335.2", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-i18next": "^17.0.4", "react-syntax-highlighter": "^16.1.0", "tailwind-merge": "^3.4.0", "three": "^0.182.0", diff --git a/apps/web/src/components/editor/ExportDialog.tsx b/apps/web/src/components/editor/ExportDialog.tsx index 026f8b93..ae2de90d 100644 --- a/apps/web/src/components/editor/ExportDialog.tsx +++ b/apps/web/src/components/editor/ExportDialog.tsx @@ -38,6 +38,7 @@ import { SelectContent, SelectItem, } from "@openreel/ui"; +import { useTranslation } from "react-i18next"; import { exportPresetsManager, type PlatformExportPreset, @@ -92,14 +93,17 @@ function getRecommendedPresetsForAspectRatio( }); } -function getAspectRatioLabel(aspectType: AspectRatioType): string { +function getAspectRatioLabel( + aspectType: AspectRatioType, + t: (key: string) => string, +): string { switch (aspectType) { case "vertical": - return "Vertical (TikTok, Reels, Shorts)"; + return t("editor:exportDialog.aspectRatioLabels.vertical"); case "square": - return "Square (Instagram Feed)"; + return t("editor:exportDialog.aspectRatioLabels.square"); case "horizontal": - return "Horizontal (YouTube, Twitter)"; + return t("editor:exportDialog.aspectRatioLabels.horizontal"); } } @@ -125,6 +129,7 @@ export const ExportDialog: React.FC = ({ projectWidth = 1920, projectHeight = 1080, }) => { + const { t } = useTranslation(); const [activeTab, setActiveTab] = useState<"presets" | "custom">("presets"); const [selectedPlatform, setSelectedPlatform] = useState( "recommended", @@ -278,7 +283,7 @@ export const ExportDialog: React.FC = ({
- Export Video + {t("editor:exportDialog.title")}
@@ -294,14 +299,14 @@ export const ExportDialog: React.FC = ({ className="flex-1 flex items-center justify-center gap-2 p-3 text-sm font-medium rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:text-primary text-text-secondary hover:text-text-primary" > - Presets + {t("editor:exportDialog.tabs.presets")} - Custom + {t("editor:exportDialog.tabs.custom")} @@ -317,10 +322,10 @@ export const ExportDialog: React.FC = ({ >
- For Your Video + {t("editor:exportDialog.recommendedForYourVideo")}
- {getAspectRatioLabel(aspectType)} + {getAspectRatioLabel(aspectType, t)}
@@ -385,7 +390,7 @@ export const ExportDialog: React.FC = ({ {preset.maxDuration && (
- Max {preset.maxDuration}s + {t("editor:exportDialog.status.maxDuration", { seconds: preset.maxDuration })}
)} @@ -398,7 +403,7 @@ export const ExportDialog: React.FC = ({
= ({
= ({
= ({ className="w-full" />
- Smaller + {t("editor:exportDialog.qualityScale.smaller")} {customSettings.quality}% - Better + {t("editor:exportDialog.qualityScale.better")}
setName(e.target.value)} - placeholder="My Awesome Template" + placeholder={t("editor:saveTemplateDialog.placeholders.templateName")} className="bg-background-secondary border-border text-text-primary" maxLength={50} /> @@ -199,12 +202,12 @@ export const SaveTemplateDialog: React.FC = ({