mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat(web): translations containing html (#10491)
* feat(web): translations containing html * add tests and more translations * more translations * rename FormatTags --> FormatMessage * update version_announcement_message
This commit is contained in:
parent
1129020159
commit
b3252ffdac
16 changed files with 313 additions and 101 deletions
|
|
@ -5,7 +5,8 @@
|
|||
import { serverConfig } from '$lib/stores/server-config.store';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Checkbox from '$lib/components/elements/checkbox.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { json, t } from 'svelte-i18n';
|
||||
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
|
||||
|
|
@ -54,12 +55,19 @@
|
|||
<div class="flex flex-col gap-4">
|
||||
{#if forceDelete}
|
||||
<p>
|
||||
<b>{user.name}</b>'s account and assets will be queued for permanent deletion <b>immediately</b>.
|
||||
<FormatMessage message={$json('admin.user_delete_immediately')} values={{ user: user.name }} let:message>
|
||||
<b>{message}</b>
|
||||
</FormatMessage>
|
||||
</p>
|
||||
{:else}
|
||||
<p>
|
||||
<b>{user.name}</b>'s account and assets will be scheduled for permanent deletion in {$serverConfig.userDeleteDelay}
|
||||
days.
|
||||
<FormatMessage
|
||||
message={$json('admin.user_delete_delay')}
|
||||
values={{ user: user.name, delay: $serverConfig.userDeleteDelay }}
|
||||
let:message
|
||||
>
|
||||
<b>{message}</b>
|
||||
</FormatMessage>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
<script lang="ts">
|
||||
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
||||
import { AppRoute, OpenSettingQueryParameterValue, QueryParameter } from '$lib/constants';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { json, t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
Apply the current
|
||||
<a
|
||||
href="{AppRoute.ADMIN_SETTINGS}?{QueryParameter.IS_OPEN}={OpenSettingQueryParameterValue.STORAGE_TEMPLATE}"
|
||||
class="text-immich-primary dark:text-immich-dark-primary"
|
||||
<FormatMessage
|
||||
message={$json('admin.storage_template_migration_description')}
|
||||
values={{ template: $t('admin.storage_template_settings') }}
|
||||
let:message
|
||||
>
|
||||
{$t('admin.storage_template_settings')}
|
||||
</a>
|
||||
to previously uploaded assets
|
||||
<a
|
||||
href="{AppRoute.ADMIN_SETTINGS}?{QueryParameter.IS_OPEN}={OpenSettingQueryParameterValue.STORAGE_TEMPLATE}"
|
||||
class="text-immich-primary dark:text-immich-dark-primary"
|
||||
>
|
||||
{message}
|
||||
</a>
|
||||
</FormatMessage>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
<script lang="ts">
|
||||
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
||||
import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { restoreUserAdmin, type UserResponseDto } from '@immich/sdk';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { json, t } from 'svelte-i18n';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
|
||||
|
|
@ -36,6 +37,10 @@
|
|||
onCancel={() => dispatch('cancel')}
|
||||
>
|
||||
<svelte:fragment slot="prompt">
|
||||
<p><b>{user.name}</b>'s account will be restored.</p>
|
||||
<p>
|
||||
<FormatMessage message={$json('admin.user_restore_description')} values={{ user: user.name }} let:message>
|
||||
<b>{message}</b>
|
||||
</FormatMessage>
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
</ConfirmDialog>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@
|
|||
import { createEventDispatcher } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import type { SettingsEventType } from '../admin-settings';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { json, t } from 'svelte-i18n';
|
||||
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
||||
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
|
|
@ -52,15 +53,16 @@
|
|||
<div class="flex flex-col gap-4">
|
||||
<p>Are you sure you want to disable all login methods? Login will be completely disabled.</p>
|
||||
<p>
|
||||
To re-enable, use a
|
||||
<a
|
||||
href="https://immich.app/docs/administration/server-commands"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
class="underline"
|
||||
>
|
||||
Server Command</a
|
||||
>.
|
||||
<FormatMessage message={$json('admin.authentication_settings_reenable')} let:message>
|
||||
<a
|
||||
href="https://immich.app/docs/administration/server-commands"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
class="underline"
|
||||
>
|
||||
{message}
|
||||
</a>
|
||||
</FormatMessage>
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
|
@ -78,12 +80,16 @@
|
|||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
For more details about this feature, refer to the <a
|
||||
href="https://immich.app/docs/administration/oauth"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer">docs</a
|
||||
>.
|
||||
<FormatMessage message={$json('admin.oauth_settings_more_details')} let:message>
|
||||
<a
|
||||
href="https://immich.app/docs/administration/oauth"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{message}
|
||||
</a>
|
||||
</FormatMessage>
|
||||
</p>
|
||||
|
||||
<SettingSwitch
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@
|
|||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import SettingCheckboxes from '$lib/components/shared-components/settings/setting-checkboxes.svelte';
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { json, t } from 'svelte-i18n';
|
||||
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
||||
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
|
|
@ -38,17 +39,21 @@
|
|||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
<Icon path={mdiHelpCircleOutline} class="inline" size="15" />
|
||||
To learn more about the terminology used here, refer to FFmpeg documentation for
|
||||
<a href="https://trac.ffmpeg.org/wiki/Encode/H.264" class="underline" target="_blank" rel="noreferrer"
|
||||
>H.264 codec</a
|
||||
>,
|
||||
<a href="https://trac.ffmpeg.org/wiki/Encode/H.265" class="underline" target="_blank" rel="noreferrer"
|
||||
>{$t('admin.transcoding_hevc_codec')}</a
|
||||
>
|
||||
and
|
||||
<a href="https://trac.ffmpeg.org/wiki/Encode/VP9" class="underline" target="_blank" rel="noreferrer"
|
||||
>VP9 codec</a
|
||||
>.
|
||||
<FormatMessage message={$json('admin.transcoding_codecs_learn_more')} let:tag let:message>
|
||||
{#if tag === 'h264-link'}
|
||||
<a href="https://trac.ffmpeg.org/wiki/Encode/H.264" class="underline" target="_blank" rel="noreferrer">
|
||||
{message}
|
||||
</a>
|
||||
{:else if tag === 'hevc-link'}
|
||||
<a href="https://trac.ffmpeg.org/wiki/Encode/H.265" class="underline" target="_blank" rel="noreferrer">
|
||||
{message}
|
||||
</a>
|
||||
{:else if tag === 'vp9-link'}
|
||||
<a href="https://trac.ffmpeg.org/wiki/Encode/VP9" class="underline" target="_blank" rel="noreferrer">
|
||||
{message}
|
||||
</a>
|
||||
{/if}
|
||||
</FormatMessage>
|
||||
</p>
|
||||
|
||||
<SettingInputField
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@
|
|||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { json, t } from 'svelte-i18n';
|
||||
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
||||
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
|
|
@ -99,12 +100,11 @@
|
|||
>
|
||||
<svelte:fragment slot="desc">
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
Set the scanning interval using the cron format. For more information please refer to e.g. <a
|
||||
href="https://crontab.guru"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer">{$t('admin.crontab_guru')}</a
|
||||
>
|
||||
<FormatMessage message={$json('admin.library_cron_expression_description')} let:message>
|
||||
<a href="https://crontab.guru" class="underline" target="_blank" rel="noreferrer">
|
||||
{message}
|
||||
</a>
|
||||
</FormatMessage>
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
</SettingInputField>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@
|
|||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { featureFlags } from '$lib/stores/server-config.store';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { json, t } from 'svelte-i18n';
|
||||
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
||||
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
|
|
@ -70,8 +71,9 @@
|
|||
isEdited={config.machineLearning.clip.modelName !== savedConfig.machineLearning.clip.modelName}
|
||||
>
|
||||
<p slot="desc" class="immich-form-label pb-2 text-sm">
|
||||
The name of a CLIP model listed <a href="https://huggingface.co/immich-app"><u>here</u></a>. Note that you
|
||||
must re-run the 'Smart Search' job for all images upon changing a model.
|
||||
<FormatMessage message={$json('admin.machine_learning_clip_model_description')} let:message>
|
||||
<a href="https://huggingface.co/immich-app"><u>{message}</u></a>
|
||||
</FormatMessage>
|
||||
</p>
|
||||
</SettingInputField>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@
|
|||
SettingInputFieldType,
|
||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { json, t } from 'svelte-i18n';
|
||||
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
||||
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
|
|
@ -88,21 +89,27 @@
|
|||
<section class="dark:text-immich-dark-fg mt-2">
|
||||
<div in:fade={{ duration: 500 }} class="mx-4 flex flex-col gap-4 py-4">
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
For more details about this feature, refer to the <a
|
||||
href="https://immich.app/docs/administration/storage-template"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>Storage Template
|
||||
</a>
|
||||
and its
|
||||
<a
|
||||
href="https://immich.app/docs/administration/backup-and-restore#asset-types-and-storage-locations"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>implications
|
||||
</a>
|
||||
<FormatMessage message={$json('admin.storage_template_more_details')} let:tag let:message>
|
||||
{#if tag === 'template-link'}
|
||||
<a
|
||||
href="https://immich.app/docs/administration/storage-template"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{message}
|
||||
</a>
|
||||
{:else if tag === 'implications-link'}
|
||||
<a
|
||||
href="https://immich.app/docs/administration/backup-and-restore#asset-types-and-storage-locations"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{message}
|
||||
</a>
|
||||
{/if}
|
||||
</FormatMessage>
|
||||
</p>
|
||||
</div>
|
||||
{#await getTemplateOptions() then}
|
||||
|
|
@ -153,15 +160,23 @@
|
|||
</div>
|
||||
|
||||
<p class="text-sm">
|
||||
Approximately path length limit : <span
|
||||
class="font-semibold text-immich-primary dark:text-immich-dark-primary"
|
||||
>{parsedTemplate().length + $user.id.length + 'UPLOAD_LOCATION'.length}</span
|
||||
>/260
|
||||
<FormatMessage
|
||||
message={$json('admin.storage_template_path_length')}
|
||||
values={{ length: parsedTemplate().length + $user.id.length + 'UPLOAD_LOCATION'.length, limit: 260 }}
|
||||
let:message
|
||||
>
|
||||
<span class="font-semibold text-immich-primary dark:text-immich-dark-primary">{message}</span>
|
||||
</FormatMessage>
|
||||
</p>
|
||||
|
||||
<p class="text-sm">
|
||||
<code class="text-immich-primary dark:text-immich-dark-primary">{$user.storageLabel || $user.id}</code> is the
|
||||
user's Storage Label
|
||||
<FormatMessage
|
||||
message={$json('admin.storage_template_user_label')}
|
||||
values={{ label: $user.storageLabel || $user.id }}
|
||||
let:message
|
||||
>
|
||||
<code class="text-immich-primary dark:text-immich-dark-primary">{message}</code>
|
||||
</FormatMessage>
|
||||
</p>
|
||||
|
||||
<p class="p-4 py-2 mt-2 text-xs bg-gray-200 rounded-lg dark:bg-gray-700 dark:text-immich-dark-fg">
|
||||
|
|
@ -213,20 +228,15 @@
|
|||
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">{$t('notes')}</h3>
|
||||
<section class="flex flex-col gap-2">
|
||||
<p>
|
||||
Template changes will only apply to new assets. To retroactively apply the template to previously
|
||||
uploaded assets, run the
|
||||
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary"
|
||||
>{$t('admin.storage_template_migration_job')}</a
|
||||
>.
|
||||
</p>
|
||||
<p>
|
||||
The template variable <span class="font-mono">{`{{album}}`}</span> will always be empty for new
|
||||
assets, so manually running the
|
||||
|
||||
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary"
|
||||
>{$t('admin.storage_template_migration_job')}</a
|
||||
<FormatMessage
|
||||
message={$json('admin.storage_template_migration_info')}
|
||||
values={{ job: $t('admin.storage_template_migration_job') }}
|
||||
let:message
|
||||
>
|
||||
is required in order to successfully use the variable.
|
||||
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary">
|
||||
{message}
|
||||
</a>
|
||||
</FormatMessage>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
78
web/src/lib/components/i18n/__test__/format-message.spec.ts
Normal file
78
web/src/lib/components/i18n/__test__/format-message.spec.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import FormatTagB from '$lib/components/i18n/__test__/format-tag-b.svelte';
|
||||
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import { init, json, locale, register, waitLocale } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { describe } from 'vitest';
|
||||
|
||||
describe('FormatMessage component', () => {
|
||||
let $json: (id: string, locale?: string | undefined) => unknown;
|
||||
|
||||
beforeAll(async () => {
|
||||
register('en', () =>
|
||||
Promise.resolve({
|
||||
hello: 'Hello {name}',
|
||||
html: 'Hello <b>{name}</b>',
|
||||
plural: 'You have <b>{count, plural, one {# item} other {# items}}</b>',
|
||||
xss: '<image/src/onerror=prompt(8)>',
|
||||
}),
|
||||
);
|
||||
|
||||
await init({ fallbackLocale: 'en' });
|
||||
await waitLocale('en');
|
||||
$json = get(json);
|
||||
});
|
||||
|
||||
it('formats a plain text message', () => {
|
||||
render(FormatMessage, {
|
||||
message: $json('hello'),
|
||||
values: { name: 'test' },
|
||||
});
|
||||
expect(screen.getByText('Hello test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('throws an error when locale is empty', async () => {
|
||||
await locale.set(undefined);
|
||||
expect(() => render(FormatMessage, { message: undefined })).toThrowError();
|
||||
await locale.set('en');
|
||||
});
|
||||
|
||||
it('shows raw message when value is empty', () => {
|
||||
render(FormatMessage, {
|
||||
message: $json('hello'),
|
||||
});
|
||||
expect(screen.getByText('Hello {name}')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows message when slot is empty', () => {
|
||||
render(FormatMessage, {
|
||||
message: $json('html'),
|
||||
values: { name: 'test' },
|
||||
});
|
||||
expect(screen.getByText('Hello test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a message with html', () => {
|
||||
const { container } = render(FormatTagB, {
|
||||
message: $json('html'),
|
||||
values: { name: 'test' },
|
||||
});
|
||||
expect(container.innerHTML).toBe('Hello <strong>test</strong>');
|
||||
});
|
||||
|
||||
it('renders a message with html and plural', () => {
|
||||
const { container } = render(FormatTagB, {
|
||||
message: $json('plural'),
|
||||
values: { count: 1 },
|
||||
});
|
||||
expect(container.innerHTML).toBe('You have <strong>1 item</strong>');
|
||||
});
|
||||
|
||||
it('protects agains XSS injection', () => {
|
||||
render(FormatMessage, {
|
||||
message: $json('xss'),
|
||||
});
|
||||
expect(screen.getByText('<image/src/onerror=prompt(8)>')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
13
web/src/lib/components/i18n/__test__/format-tag-b.svelte
Normal file
13
web/src/lib/components/i18n/__test__/format-tag-b.svelte
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import FormatMessage from '../format-message.svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
export let message: unknown;
|
||||
export let values: ComponentProps<FormatMessage>['values'];
|
||||
</script>
|
||||
|
||||
<FormatMessage {message} {values} let:tag let:message>
|
||||
{#if tag === 'b'}
|
||||
<strong>{message}</strong>
|
||||
{/if}
|
||||
</FormatMessage>
|
||||
57
web/src/lib/components/i18n/format-message.svelte
Normal file
57
web/src/lib/components/i18n/format-message.svelte
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<script lang="ts">
|
||||
import { IntlMessageFormat, type FormatXMLElementFn, type PrimitiveType } from 'intl-messageformat';
|
||||
import { TYPE, type MessageFormatElement } from '@formatjs/icu-messageformat-parser';
|
||||
import { locale as i18nLocale } from 'svelte-i18n';
|
||||
|
||||
type InterpolationValues = Record<string, PrimitiveType | FormatXMLElementFn<unknown>>;
|
||||
|
||||
export let message: unknown;
|
||||
export let values: InterpolationValues = {};
|
||||
|
||||
const getLocale = (locale?: string | null) => {
|
||||
if (locale == null) {
|
||||
throw new Error('Cannot format a message without first setting the initial locale.');
|
||||
}
|
||||
|
||||
return locale;
|
||||
};
|
||||
|
||||
const getElements = (message: unknown, locale: string): MessageFormatElement[] => {
|
||||
return new IntlMessageFormat(message as string, locale, undefined, {
|
||||
ignoreTag: false,
|
||||
}).getAst();
|
||||
};
|
||||
|
||||
const getParts = (message: unknown, locale: string) => {
|
||||
try {
|
||||
const elements = getElements(message, locale);
|
||||
|
||||
return elements.map((element) => {
|
||||
const isTag = element.type === TYPE.tag;
|
||||
|
||||
return {
|
||||
tag: isTag ? element.value : undefined,
|
||||
message: new IntlMessageFormat(isTag ? element.children : [element], locale, undefined, {
|
||||
ignoreTag: true,
|
||||
}).format(values) as string,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.warn(`Message "${message}" has syntax error:`, error.message);
|
||||
}
|
||||
return [{ message: message as string, tag: undefined }];
|
||||
}
|
||||
};
|
||||
|
||||
$: locale = getLocale($i18nLocale);
|
||||
$: parts = getParts(message, locale);
|
||||
</script>
|
||||
|
||||
{#each parts as { tag, message }}
|
||||
{#if tag}
|
||||
<slot {tag} {message}>{message}</slot>
|
||||
{:else}
|
||||
{message}
|
||||
{/if}
|
||||
{/each}
|
||||
|
|
@ -9,7 +9,8 @@
|
|||
import Button from '../elements/buttons/button.svelte';
|
||||
import Icon from '../elements/icon.svelte';
|
||||
import OnboardingCard from './onboarding-card.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { json, t } from 'svelte-i18n';
|
||||
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
done: void;
|
||||
|
|
@ -29,9 +30,9 @@
|
|||
</p>
|
||||
|
||||
<p>
|
||||
When enabled, this feature will auto-organize files based on a user-defined template. Due to stability issues the
|
||||
feature has been turned off by default. For more information, please see the
|
||||
<a class="underline" href="https://immich.app/docs/administration/storage-template">documentation</a>.
|
||||
<FormatMessage message={$json('admin.storage_template_onboarding_description')} let:message>
|
||||
<a class="underline" href="https://immich.app/docs/administration/storage-template">{message}</a>
|
||||
</FormatMessage>
|
||||
</p>
|
||||
|
||||
{#if config && $user}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
import type { ServerVersionResponseDto } from '@immich/sdk';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import FullScreenModal from './full-screen-modal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { json, t } from 'svelte-i18n';
|
||||
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
||||
|
||||
let showModal = false;
|
||||
|
||||
|
|
@ -36,14 +37,17 @@
|
|||
{#if showModal}
|
||||
<FullScreenModal title="🎉 NEW VERSION AVAILABLE" onClose={() => (showModal = false)}>
|
||||
<div>
|
||||
Hi friend, there is a new version of the application please take your time to visit the
|
||||
<span class="font-medium underline"
|
||||
><a href="https://github.com/immich-app/immich/releases/latest" target="_blank" rel="noopener noreferrer"
|
||||
>release notes</a
|
||||
></span
|
||||
>
|
||||
and ensure your <code>docker-compose</code>, and <code>.env</code> setup is up-to-date to prevent any misconfigurations,
|
||||
especially if you use WatchTower or any mechanism that handles updating your application automatically.
|
||||
<FormatMessage message={$json('version_announcement_message')} let:tag let:message>
|
||||
{#if tag === 'link'}
|
||||
<span class="font-medium underline">
|
||||
<a href="https://github.com/immich-app/immich/releases/latest" target="_blank" rel="noopener noreferrer">
|
||||
{message}
|
||||
</a>
|
||||
</span>
|
||||
{:else if tag === 'code'}
|
||||
<code>{message}</code>
|
||||
{/if}
|
||||
</FormatMessage>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 font-medium">Your friend, Alex</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue