Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions src-tauri/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleAllowMixedLocalizations</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>de</string>
<string>es</string>
<string>fr</string>
<string>it</string>
<string>ja</string>
<string>ko</string>
<string>pl</string>
<string>pt</string>
<string>ru</string>
<string>zh-Hans</string>
<string>zh-Hant</string>
</array>
</dict>
</plist>
9 changes: 5 additions & 4 deletions src/components/modals/ImportSettingsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,14 @@ export default function ImportSettingsModal({ fileCount, isOpen, onClose, onSave
value={filenameTemplate}
/>
<div className="flex flex-wrap gap-2 mt-2">
{FILENAME_VARIABLES.map((variable: string) => (
{FILENAME_VARIABLES.map((variable) => (
<button
className="px-2 py-1 bg-surface text-text-secondary text-xs rounded-md hover:bg-card-active transition-colors"
key={variable}
onClick={() => handleVariableClick(variable)}
key={variable.token}
onClick={() => handleVariableClick(variable.token)}
data-tooltip={variable.token}
>
{variable}
{`{${t(variable.labelKey)}}`}
</button>
))}
</div>
Expand Down
9 changes: 5 additions & 4 deletions src/components/modals/RenameFileModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,14 @@ export default function RenameFileModal({ filesToRename, isOpen, onClose, onSave
/>
{!isSingleFile && (
<div className="flex flex-wrap gap-2 mt-2">
{FILENAME_VARIABLES.map((variable: string) => (
{FILENAME_VARIABLES.map((variable) => (
<button
className="px-2 py-1 bg-surface text-text-secondary text-xs rounded-md hover:bg-card-active transition-colors"
key={variable}
onClick={() => handleVariableClick(variable)}
key={variable.token}
onClick={() => handleVariableClick(variable.token)}
data-tooltip={variable.token}
>
{variable}
{`{${t(variable.labelKey)}}`}
</button>
))}
</div>
Expand Down
11 changes: 6 additions & 5 deletions src/components/panel/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import Text from '../ui/Text';
import { TextColors, TextVariants, TextWeights } from '../../types/typography';
import { useOsPlatform } from '../../hooks/useOsPlatform';
import { open } from '@tauri-apps/plugin-shell';
import { translateBuiltInTag } from '../../i18n/translateBuiltInTag';

interface ConfirmModalState {
confirmText: string;
Expand Down Expand Up @@ -1466,11 +1467,13 @@ export default function SettingsPanel({
animate="visible"
exit="exit"
onClick={() => handleRemoveShortcut(shortcut)}
data-tooltip={t('settings.tagging.removeShortcutTooltip', { shortcut })}
data-tooltip={t('settings.tagging.removeShortcutTooltip', {
shortcut: translateBuiltInTag(shortcut),
})}
className="flex items-center gap-1 bg-surface px-2 py-1 rounded-sm group cursor-pointer"
>
<Text variant={TextVariants.label} color={TextColors.primary}>
{shortcut}
{translateBuiltInTag(shortcut)}
</Text>
<span className="rounded-full group-hover:bg-black/20 p-0.5 transition-colors">
<X size={14} />
Expand Down Expand Up @@ -2267,9 +2270,7 @@ export default function SettingsPanel({
buttonText={t('settings.data.clearSidecarsButton')}
description={
<Text as="span" variant={TextVariants.small}>
{t('settings.data.clearSidecarsDesc')}{' '}
<code className="bg-bg-primary px-1 rounded-sm text-text-primary">.rrdata</code> files
(containing your edits) within your root folders:
{t('settings.data.clearSidecarsDesc')}
<span className="block font-mono bg-bg-primary p-2 rounded-sm mt-2 break-all border border-border-color whitespace-pre-wrap">
{effectiveRootPaths.length > 0
? effectiveRootPaths.join('\n')
Expand Down
9 changes: 5 additions & 4 deletions src/components/panel/right/ExportPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -619,14 +619,15 @@ export default function ExportPanel({
value={filenameTemplate}
/>
<div className="flex flex-wrap gap-2 mt-2">
{FILENAME_VARIABLES.map((variable: string) => (
{FILENAME_VARIABLES.map((variable) => (
<button
className="px-2 py-1 bg-surface text-text-secondary text-xs rounded-md hover:bg-card-active transition-colors disabled:opacity-50"
disabled={isExporting}
key={variable}
onClick={() => handleVariableClick(variable)}
key={variable.token}
onClick={() => handleVariableClick(variable.token)}
data-tooltip={variable.token}
>
{variable}
{`{${t(variable.labelKey)}}`}
</button>
))}
</div>
Expand Down
166 changes: 163 additions & 3 deletions src/components/panel/right/MetadataPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState, useMemo, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { Check, ChevronDown, ChevronRight, Plus, Star, Tag, X, User } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import i18n from 'i18next';
import { useTranslation } from 'react-i18next';
import clsx from 'clsx';
import { Invokes } from '../../ui/AppProperties';
Expand All @@ -14,6 +15,7 @@ import { useLibraryStore } from '../../../store/useLibraryStore';
import { useSettingsStore } from '../../../store/useSettingsStore';
import { useProcessStore } from '../../../store/useProcessStore';
import { useLibraryActions } from '../../../hooks/useLibraryActions';
import { translateBuiltInTag } from '../../../i18n/translateBuiltInTag';

interface CameraSetting {
format?(value: number): string | number;
Expand Down Expand Up @@ -47,6 +49,160 @@ function formatExifTag(str: string) {
return str.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/([A-Z])([A-Z][a-z])/g, '$1 $2');
}

const EXIF_FIELD_TRANSLATION_KEYS: Record<string, string> = {
ApertureValue: 'editor.metadata.extendedExif.fields.apertureValue',
Artist: 'editor.metadata.extendedExif.fields.artist',
BitsPerSample: 'editor.metadata.extendedExif.fields.bitsPerSample',
BodySerialNumber: 'editor.metadata.extendedExif.fields.bodySerialNumber',
BrightnessValue: 'editor.metadata.extendedExif.fields.brightnessValue',
CameraSerialNumber: 'editor.metadata.extendedExif.fields.cameraSerialNumber',
ColorSpace: 'editor.metadata.extendedExif.fields.colorSpace',
ComponentsConfiguration: 'editor.metadata.extendedExif.fields.componentsConfiguration',
Compression: 'editor.metadata.extendedExif.fields.compression',
Contrast: 'editor.metadata.extendedExif.fields.contrast',
Copyright: 'editor.metadata.extendedExif.fields.copyright',
CreateDate: 'editor.metadata.extendedExif.fields.createDate',
CustomRendered: 'editor.metadata.extendedExif.fields.customRendered',
DateTimeOriginal: 'editor.metadata.extendedExif.fields.dateTimeOriginal',
DigitalZoomRatio: 'editor.metadata.extendedExif.fields.digitalZoomRatio',
ExifVersion: 'editor.metadata.extendedExif.fields.exifVersion',
ExposureBiasValue: 'editor.metadata.extendedExif.fields.exposureBiasValue',
ExposureIndex: 'editor.metadata.extendedExif.fields.exposureIndex',
ExposureMode: 'editor.metadata.extendedExif.fields.exposureMode',
ExposureProgram: 'editor.metadata.extendedExif.fields.exposureProgram',
FileSource: 'editor.metadata.extendedExif.fields.fileSource',
Flash: 'editor.metadata.extendedExif.fields.flash',
FlashpixVersion: 'editor.metadata.extendedExif.fields.flashpixVersion',
FocalLength: 'editor.metadata.extendedExif.fields.focalLength',
FocalLengthIn35mmFormat: 'editor.metadata.extendedExif.fields.focalLengthIn35mmFormat',
FocalPlaneResolutionUnit: 'editor.metadata.extendedExif.fields.focalPlaneResolutionUnit',
FocalPlaneXResolution: 'editor.metadata.extendedExif.fields.focalPlaneXResolution',
FocalPlaneYResolution: 'editor.metadata.extendedExif.fields.focalPlaneYResolution',
GainControl: 'editor.metadata.extendedExif.fields.gainControl',
ImageDescription: 'editor.metadata.extendedExif.fields.imageDescription',
ImageHeight: 'editor.metadata.extendedExif.fields.imageHeight',
ImageLength: 'editor.metadata.extendedExif.fields.imageLength',
ImageUniqueID: 'editor.metadata.extendedExif.fields.imageUniqueId',
ImageWidth: 'editor.metadata.extendedExif.fields.imageWidth',
ISOSpeed: 'editor.metadata.extendedExif.fields.isoSpeed',
LensMake: 'editor.metadata.extendedExif.fields.lensMake',
LensModel: 'editor.metadata.extendedExif.fields.lensModel',
LensSerialNumber: 'editor.metadata.extendedExif.fields.lensSerialNumber',
LensSpecification: 'editor.metadata.extendedExif.fields.lensSpecification',
LightSource: 'editor.metadata.extendedExif.fields.lightSource',
Make: 'editor.metadata.extendedExif.fields.make',
MaxApertureValue: 'editor.metadata.extendedExif.fields.maxApertureValue',
MeteringMode: 'editor.metadata.extendedExif.fields.meteringMode',
Model: 'editor.metadata.extendedExif.fields.model',
ModifyDate: 'editor.metadata.extendedExif.fields.modifyDate',
OffsetTime: 'editor.metadata.extendedExif.fields.offsetTime',
OffsetTimeDigitized: 'editor.metadata.extendedExif.fields.offsetTimeDigitized',
OffsetTimeOriginal: 'editor.metadata.extendedExif.fields.offsetTimeOriginal',
Orientation: 'editor.metadata.extendedExif.fields.orientation',
PhotometricInterpretation: 'editor.metadata.extendedExif.fields.photometricInterpretation',
PhotographicSensitivity: 'editor.metadata.extendedExif.fields.photographicSensitivity',
PixelXDimension: 'editor.metadata.extendedExif.fields.pixelXDimension',
PixelYDimension: 'editor.metadata.extendedExif.fields.pixelYDimension',
PlanarConfiguration: 'editor.metadata.extendedExif.fields.planarConfiguration',
RecommendedExposureIndex: 'editor.metadata.extendedExif.fields.recommendedExposureIndex',
ResolutionUnit: 'editor.metadata.extendedExif.fields.resolutionUnit',
RowsPerStrip: 'editor.metadata.extendedExif.fields.rowsPerStrip',
Saturation: 'editor.metadata.extendedExif.fields.saturation',
SamplesPerPixel: 'editor.metadata.extendedExif.fields.samplesPerPixel',
SceneCaptureType: 'editor.metadata.extendedExif.fields.sceneCaptureType',
SceneType: 'editor.metadata.extendedExif.fields.sceneType',
SensingMethod: 'editor.metadata.extendedExif.fields.sensingMethod',
SensitivityType: 'editor.metadata.extendedExif.fields.sensitivityType',
Sharpness: 'editor.metadata.extendedExif.fields.sharpness',
ShutterSpeedValue: 'editor.metadata.extendedExif.fields.shutterSpeedValue',
Software: 'editor.metadata.extendedExif.fields.software',
StripByteCounts: 'editor.metadata.extendedExif.fields.stripByteCounts',
StripOffsets: 'editor.metadata.extendedExif.fields.stripOffsets',
SubSecTimeDigitized: 'editor.metadata.extendedExif.fields.subSecTimeDigitized',
SubSecTimeOriginal: 'editor.metadata.extendedExif.fields.subSecTimeOriginal',
SubjectDistance: 'editor.metadata.extendedExif.fields.subjectDistance',
UserComment: 'editor.metadata.extendedExif.fields.userComment',
WhiteBalance: 'editor.metadata.extendedExif.fields.whiteBalance',
XResolution: 'editor.metadata.extendedExif.fields.xResolution',
YResolution: 'editor.metadata.extendedExif.fields.yResolution',
};

const EXIF_VALUE_TRANSLATION_KEYS: Record<string, Record<string, string>> = {
Compression: {
uncompressed: 'editor.metadata.extendedExif.values.uncompressed',
},
Contrast: {
normal: 'editor.metadata.extendedExif.values.normal',
},
CustomRendered: {
'normal process': 'editor.metadata.extendedExif.values.normalProcess',
},
ExposureMode: {
'auto exposure': 'editor.metadata.extendedExif.values.autoExposure',
'manual exposure': 'editor.metadata.extendedExif.values.manualExposure',
},
ExposureProgram: {
manual: 'editor.metadata.extendedExif.values.manual',
},
FileSource: {
'digital still camera': 'editor.metadata.extendedExif.values.digitalStillCamera',
},
Flash: {
'fired, return light detected': 'editor.metadata.extendedExif.values.flashFiredReturnDetected',
'fired, return light not detected': 'editor.metadata.extendedExif.values.flashFiredReturnNotDetected',
'fired, return light not detected, forced': 'editor.metadata.extendedExif.values.flashFiredReturnNotDetectedForced',
'flash did not fire': 'editor.metadata.extendedExif.values.flashDidNotFire',
'no flash': 'editor.metadata.extendedExif.values.noFlash',
},
MeteringMode: {
pattern: 'editor.metadata.extendedExif.values.patternMetering',
},
Orientation: {
'row 0 at top and column 0 at left': 'editor.metadata.extendedExif.values.topLeft',
},
PlanarConfiguration: {
chunky: 'editor.metadata.extendedExif.values.chunky',
},
Saturation: {
normal: 'editor.metadata.extendedExif.values.normal',
},
SceneCaptureType: {
landscape: 'editor.metadata.extendedExif.values.landscape',
portrait: 'editor.metadata.extendedExif.values.portrait',
standard: 'editor.metadata.extendedExif.values.standard',
},
SceneType: {
'directly photographed': 'editor.metadata.extendedExif.values.directlyPhotographed',
'directly photographed image': 'editor.metadata.extendedExif.values.directlyPhotographed',
},
Sharpness: {
normal: 'editor.metadata.extendedExif.values.normal',
},
WhiteBalance: {
auto: 'editor.metadata.extendedExif.values.auto',
manual: 'editor.metadata.extendedExif.values.manual',
},
};

function translateExtendedExifTag(tag: string): string {
const translationKey = EXIF_FIELD_TRANSLATION_KEYS[tag];
if (translationKey) return String(i18n.t(translationKey as never));

const tiffTagMatch = tag.match(/^Tag\(Tiff,\s*(\d+)\)$/i);
if (tiffTagMatch) {
return String(i18n.t('editor.metadata.extendedExif.unknownTiffTag', { id: tiffTagMatch[1] }));
}

return formatExifTag(tag);
}

function translateExtendedExifValue(tag: string, value: unknown) {
if (typeof value !== 'string') return value;
const normalizedValue = value.trim().toLowerCase().replace(/\s+/g, ' ');
const translationKey = EXIF_VALUE_TRANSLATION_KEYS[tag]?.[normalizedValue];
return translationKey ? String(i18n.t(translationKey as never)) : value;
}

function parseDms(dmsString: string) {
if (!dmsString) return null;
const parts = dmsString.match(/(\d+\.?\d*)\s+deg\s+(\d+\.?\d*)\s+min\s+(\d+\.?\d*)\s+sec/);
Expand Down Expand Up @@ -681,7 +837,7 @@ export default function MetadataPanel() {
color={TextColors.primary}
weight={TextWeights.medium}
>
{tagItem.tag}
{translateBuiltInTag(tagItem.tag)}
</Text>
<X size={10} className="opacity-50 group-hover:opacity-100" />
</motion.div>
Expand Down Expand Up @@ -726,7 +882,7 @@ export default function MetadataPanel() {
onClick={() => handleAddTag(shortcut)}
className="text-xs font-medium bg-bg-secondary hover:bg-card-active text-text-secondary px-1.5 py-0.5 rounded-sm border border-transparent hover:border-border-color transition-all"
>
{shortcut}
{translateBuiltInTag(shortcut)}
</button>
))}
</div>
Expand Down Expand Up @@ -787,7 +943,11 @@ export default function MetadataPanel() {
</Text>
<div className="bg-surface border border-surface rounded-xl p-3 flex flex-col gap-0.5 overflow-hidden">
{otherExifEntries.map(([tag, value]) => (
<MetadataItem key={tag} label={formatExifTag(tag)} value={value} />
<MetadataItem
key={tag}
label={translateExtendedExifTag(tag)}
value={translateExtendedExifValue(tag, value)}
/>
))}
</div>
</div>
Expand Down
18 changes: 9 additions & 9 deletions src/components/ui/ExportImportProperties.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ export const FILE_FORMATS: Array<FileFormat> = [
{ id: FileFormats.Cube, name: 'CUBE LUT', extensions: ['cube'] },
];

export const FILENAME_VARIABLES: Array<string> = [
'{original_filename}',
'{sequence}',
'{YYYY}',
'{MM}',
'{DD}',
'{hh}',
'{mm}',
];
export const FILENAME_VARIABLES = [
{ token: '{original_filename}', labelKey: 'ui.filenameVariables.originalFilename' },
{ token: '{sequence}', labelKey: 'ui.filenameVariables.sequence' },
{ token: '{YYYY}', labelKey: 'ui.filenameVariables.year' },
{ token: '{MM}', labelKey: 'ui.filenameVariables.month' },
{ token: '{DD}', labelKey: 'ui.filenameVariables.day' },
{ token: '{hh}', labelKey: 'ui.filenameVariables.hour' },
{ token: '{mm}', labelKey: 'ui.filenameVariables.minute' },
] as const;

export interface ExportSettings {
filenameTemplate: string | null;
Expand Down
7 changes: 4 additions & 3 deletions src/context/TaggingSubMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
import { Invokes } from '../components/ui/AppProperties';
import Text from '../components/ui/Text';
import { TextVariants } from '../types/typography';
import { translateBuiltInTag } from '../i18n/translateBuiltInTag';

interface TaggingSubMenuProps {
paths: string[];
Expand Down Expand Up @@ -101,10 +102,10 @@ export default function TaggingSubMenu({
animate="visible"
exit="exit"
onClick={() => handleRemoveTag(tagItem)}
data-tooltip={t('menus.tagging.removeTooltip', { tag: tagItem.tag })}
data-tooltip={t('menus.tagging.removeTooltip', { tag: translateBuiltInTag(tagItem.tag) })}
className="flex items-center gap-1 bg-bg-primary text-text-primary text-xs font-medium px-2 py-1 rounded-sm group cursor-pointer"
>
<span>{tagItem.tag}</span>
<span>{translateBuiltInTag(tagItem.tag)}</span>
<span className="rounded-full group-hover:bg-black/20 p-0.5 transition-colors">
<X size={12} />
</span>
Expand Down Expand Up @@ -157,7 +158,7 @@ export default function TaggingSubMenu({
onClick={() => handleAddTag(shortcut)}
className="bg-surface text-text-secondary hover:bg-card-active hover:text-text-primary text-xs font-medium px-2 py-1 rounded-sm"
>
{shortcut}
{translateBuiltInTag(shortcut)}
</button>
))}
</div>
Expand Down
Loading