diff --git a/app/Http/Controllers/Base/LocaleController.php b/app/Http/Controllers/Base/LocaleController.php index 1842e94db..cffec156b 100644 --- a/app/Http/Controllers/Base/LocaleController.php +++ b/app/Http/Controllers/Base/LocaleController.php @@ -57,7 +57,16 @@ class LocaleController extends Controller if (is_array($value)) { $data[$key] = $this->i18n($value); } else { - $data[$key] = preg_replace('/:([\w-]+)(\W?|$)/m', '{{$1}}$2', $value); + // Find a Laravel style translation replacement in the string and replace it with + // one that the front-end is able to use. This won't always be present, especially + // for complex strings or things where we'd never have a backend component anyways. + // + // For example: + // "Hello :name, the :notifications.0.title notification needs :count actions :foo.0.bar." + // + // Becomes: + // "Hello {{name}}, the {{notifications.0.title}} notification needs {{count}} actions {{foo.0.bar}}." + $data[$key] = preg_replace('/:([\w.-]+\w)([^\w:]?|$)/m', '{{$1}}$2', $value); } } diff --git a/app/Transformers/Api/Client/ActivityLogTransformer.php b/app/Transformers/Api/Client/ActivityLogTransformer.php index 7a9b6979b..b64a23769 100644 --- a/app/Transformers/Api/Client/ActivityLogTransformer.php +++ b/app/Transformers/Api/Client/ActivityLogTransformer.php @@ -81,7 +81,7 @@ class ActivityLogTransformer extends BaseClientTransformer } $str = trans('activity.' . str_replace(':', '.', $model->event)); - preg_match_all('/:(?[\w-]+)(?:\W?|$)/', $str, $matches); + preg_match_all('/:(?[\w.-]+\w)(?:[^\w:]?|$)/', $str, $matches); $exclude = array_merge($matches['key'], ['ip', 'useragent']); foreach ($model->properties->keys() as $key) { diff --git a/resources/lang/en/activity.php b/resources/lang/en/activity.php index bcc6abaa0..f821f362b 100644 --- a/resources/lang/en/activity.php +++ b/resources/lang/en/activity.php @@ -54,17 +54,18 @@ return [ 'delete' => 'Deleted database :name', ], 'file' => [ - 'compress_one' => 'Compressed :directory/:file', + 'compress_one' => 'Compressed :directory:file', 'compress_other' => 'Compressed :count files in :directory', 'read' => 'Viewed the contents of :file', 'copy' => 'Created a copy of :file', 'create-directory' => 'Created a new directory :name in :directory', 'decompress' => 'Decompressed :files in :directory', - 'delete_one' => 'Deleted :directory/:files', + 'delete_one' => 'Deleted :directory:files', 'delete_other' => 'Deleted :count files in :directory', 'download' => 'Downloaded :file', 'pull' => 'Downloaded a remote file from :url to :directory', - 'rename' => 'Renamed files in :directory', + 'rename_one' => 'Renamed :directory:files.0.from to :directory:files.0.to', + 'rename_other' => 'Renamed :count files in :directory', 'write' => 'Wrote new content to :file', 'upload' => 'Began a file upload', ], @@ -75,7 +76,7 @@ return [ 'delete' => 'Deleted the :allocation allocation', ], 'schedule' => [ - 'store' => 'Created the :name schedule', + 'create' => 'Created the :name schedule', 'update' => 'Updated the :name schedule', 'execute' => 'Manually executed the :name schedule', 'delete' => 'Deleted the :name schedule', diff --git a/resources/scripts/components/elements/activity/ActivityLogEntry.tsx b/resources/scripts/components/elements/activity/ActivityLogEntry.tsx index cf613c9c2..9d4c2f8e5 100644 --- a/resources/scripts/components/elements/activity/ActivityLogEntry.tsx +++ b/resources/scripts/components/elements/activity/ActivityLogEntry.tsx @@ -10,12 +10,28 @@ import ActivityLogMetaButton from '@/components/elements/activity/ActivityLogMet import { TerminalIcon } from '@heroicons/react/solid'; import classNames from 'classnames'; import style from './style.module.css'; +import { isObject } from '@/helpers'; interface Props { activity: ActivityLog; children?: React.ReactNode; } +const formatProperties = (properties: Record): Record => { + return Object.keys(properties).reduce((obj, key) => { + const value = properties[key]; + // noinspection SuspiciousTypeOfGuard + const isCount = key === 'count' || (typeof key === 'string' && key.endsWith('_count')); + + return { + ...obj, + [key]: isCount || typeof value !== 'string' + ? (isObject(value) ? formatProperties(value) : value) + : `${value}`, + }; + }, {}); +}; + export default ({ activity, children }: Props) => { const location = useLocation(); const actor = activity.relationships.actor; @@ -27,12 +43,7 @@ export default ({ activity, children }: Props) => { return current.toString(); }; - const properties = Object.keys(activity.properties).reduce((obj, key) => ({ - ...obj, - [key]: key === 'count' || key.endsWith('_count') - ? activity.properties[key] - : `${activity.properties[key]}`, - }), {}); + const properties = formatProperties(activity.properties); return (
diff --git a/resources/scripts/helpers.ts b/resources/scripts/helpers.ts index fe6cc4f51..9b31aaddc 100644 --- a/resources/scripts/helpers.ts +++ b/resources/scripts/helpers.ts @@ -74,4 +74,4 @@ export const isEmptyObject = (o: {}): boolean => Object.keys(o).length === 0 && Object.getPrototypeOf(o) === Object.prototype; // eslint-disable-next-line @typescript-eslint/ban-types -export const getObjectKeys = (o: T): Array => Object.keys(o) as Array; +export const getObjectKeys = (o: T): (keyof T)[] => Object.keys(o) as (keyof typeof o)[];