1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

fix(web): FormatMessage development keys (#10536)

This commit is contained in:
Michel Heusschen 2024-06-22 18:08:56 +02:00 committed by GitHub
parent 4cb165304b
commit 6164640575
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 46 additions and 44 deletions

View file

@ -5,7 +5,7 @@
import { serverConfig } from '$lib/stores/server-config.store'; import { serverConfig } from '$lib/stores/server-config.store';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import Checkbox from '$lib/components/elements/checkbox.svelte'; import Checkbox from '$lib/components/elements/checkbox.svelte';
import { json, t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import FormatMessage from '$lib/components/i18n/format-message.svelte'; import FormatMessage from '$lib/components/i18n/format-message.svelte';
export let user: UserResponseDto; export let user: UserResponseDto;
@ -55,14 +55,14 @@
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
{#if forceDelete} {#if forceDelete}
<p> <p>
<FormatMessage message={$json('admin.user_delete_immediately')} values={{ user: user.name }} let:message> <FormatMessage key="admin.user_delete_immediately" values={{ user: user.name }} let:message>
<b>{message}</b> <b>{message}</b>
</FormatMessage> </FormatMessage>
</p> </p>
{:else} {:else}
<p> <p>
<FormatMessage <FormatMessage
message={$json('admin.user_delete_delay')} key="admin.user_delete_delay"
values={{ user: user.name, delay: $serverConfig.userDeleteDelay }} values={{ user: user.name, delay: $serverConfig.userDeleteDelay }}
let:message let:message
> >

View file

@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import FormatMessage from '$lib/components/i18n/format-message.svelte'; import FormatMessage from '$lib/components/i18n/format-message.svelte';
import { AppRoute, OpenSettingQueryParameterValue, QueryParameter } from '$lib/constants'; import { AppRoute, OpenSettingQueryParameterValue, QueryParameter } from '$lib/constants';
import { json, t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
</script> </script>
<FormatMessage <FormatMessage
message={$json('admin.storage_template_migration_description')} key="admin.storage_template_migration_description"
values={{ template: $t('admin.storage_template_settings') }} values={{ template: $t('admin.storage_template_settings') }}
let:message let:message
> >

View file

@ -4,7 +4,7 @@
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { restoreUserAdmin, type UserResponseDto } from '@immich/sdk'; import { restoreUserAdmin, type UserResponseDto } from '@immich/sdk';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { json, t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
export let user: UserResponseDto; export let user: UserResponseDto;
@ -38,7 +38,7 @@
> >
<svelte:fragment slot="prompt"> <svelte:fragment slot="prompt">
<p> <p>
<FormatMessage message={$json('admin.user_restore_description')} values={{ user: user.name }} let:message> <FormatMessage key="admin.user_restore_description" values={{ user: user.name }} let:message>
<b>{message}</b> <b>{message}</b>
</FormatMessage> </FormatMessage>
</p> </p>

View file

@ -11,7 +11,7 @@
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { SettingsEventType } from '../admin-settings'; import type { SettingsEventType } from '../admin-settings';
import { json, t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import FormatMessage from '$lib/components/i18n/format-message.svelte'; import FormatMessage from '$lib/components/i18n/format-message.svelte';
export let savedConfig: SystemConfigDto; export let savedConfig: SystemConfigDto;
@ -53,7 +53,7 @@
<div class="flex flex-col gap-4"> <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>Are you sure you want to disable all login methods? Login will be completely disabled.</p>
<p> <p>
<FormatMessage message={$json('admin.authentication_settings_reenable')} let:message> <FormatMessage key="admin.authentication_settings_reenable" let:message>
<a <a
href="https://immich.app/docs/administration/server-commands" href="https://immich.app/docs/administration/server-commands"
rel="noreferrer" rel="noreferrer"
@ -80,7 +80,7 @@
> >
<div class="ml-4 mt-4 flex flex-col gap-4"> <div class="ml-4 mt-4 flex flex-col gap-4">
<p class="text-sm dark:text-immich-dark-fg"> <p class="text-sm dark:text-immich-dark-fg">
<FormatMessage message={$json('admin.oauth_settings_more_details')} let:message> <FormatMessage key="admin.oauth_settings_more_details" let:message>
<a <a
href="https://immich.app/docs/administration/oauth" href="https://immich.app/docs/administration/oauth"
class="underline" class="underline"

View file

@ -22,7 +22,7 @@
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingCheckboxes from '$lib/components/shared-components/settings/setting-checkboxes.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 SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import { json, t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import FormatMessage from '$lib/components/i18n/format-message.svelte'; import FormatMessage from '$lib/components/i18n/format-message.svelte';
export let savedConfig: SystemConfigDto; export let savedConfig: SystemConfigDto;
@ -39,7 +39,7 @@
<div class="ml-4 mt-4 flex flex-col gap-4"> <div class="ml-4 mt-4 flex flex-col gap-4">
<p class="text-sm dark:text-immich-dark-fg"> <p class="text-sm dark:text-immich-dark-fg">
<Icon path={mdiHelpCircleOutline} class="inline" size="15" /> <Icon path={mdiHelpCircleOutline} class="inline" size="15" />
<FormatMessage message={$json('admin.transcoding_codecs_learn_more')} let:tag let:message> <FormatMessage key="admin.transcoding_codecs_learn_more" let:tag let:message>
{#if tag === 'h264-link'} {#if tag === 'h264-link'}
<a href="https://trac.ffmpeg.org/wiki/Encode/H.264" class="underline" target="_blank" rel="noreferrer"> <a href="https://trac.ffmpeg.org/wiki/Encode/H.264" class="underline" target="_blank" rel="noreferrer">
{message} {message}

View file

@ -10,7 +10,7 @@
} from '$lib/components/shared-components/settings/setting-input-field.svelte'; } from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.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 SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import { json, t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import FormatMessage from '$lib/components/i18n/format-message.svelte'; import FormatMessage from '$lib/components/i18n/format-message.svelte';
export let savedConfig: SystemConfigDto; export let savedConfig: SystemConfigDto;
@ -100,7 +100,7 @@
> >
<svelte:fragment slot="desc"> <svelte:fragment slot="desc">
<p class="text-sm dark:text-immich-dark-fg"> <p class="text-sm dark:text-immich-dark-fg">
<FormatMessage message={$json('admin.library_cron_expression_description')} let:message> <FormatMessage key="admin.library_cron_expression_description" let:message>
<a href="https://crontab.guru" class="underline" target="_blank" rel="noreferrer"> <a href="https://crontab.guru" class="underline" target="_blank" rel="noreferrer">
{message} {message}
</a> </a>

View file

@ -12,7 +12,7 @@
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte'; import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/server-config.store';
import { json, t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import FormatMessage from '$lib/components/i18n/format-message.svelte'; import FormatMessage from '$lib/components/i18n/format-message.svelte';
export let savedConfig: SystemConfigDto; export let savedConfig: SystemConfigDto;
@ -71,7 +71,7 @@
isEdited={config.machineLearning.clip.modelName !== savedConfig.machineLearning.clip.modelName} isEdited={config.machineLearning.clip.modelName !== savedConfig.machineLearning.clip.modelName}
> >
<p slot="desc" class="immich-form-label pb-2 text-sm"> <p slot="desc" class="immich-form-label pb-2 text-sm">
<FormatMessage message={$json('admin.machine_learning_clip_model_description')} let:message> <FormatMessage key="admin.machine_learning_clip_model_description" let:message>
<a href="https://huggingface.co/immich-app"><u>{message}</u></a> <a href="https://huggingface.co/immich-app"><u>{message}</u></a>
</FormatMessage> </FormatMessage>
</p> </p>

View file

@ -20,7 +20,7 @@
SettingInputFieldType, SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte'; } from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { json, t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import FormatMessage from '$lib/components/i18n/format-message.svelte'; import FormatMessage from '$lib/components/i18n/format-message.svelte';
export let savedConfig: SystemConfigDto; export let savedConfig: SystemConfigDto;
@ -89,7 +89,7 @@
<section class="dark:text-immich-dark-fg mt-2"> <section class="dark:text-immich-dark-fg mt-2">
<div in:fade={{ duration: 500 }} class="mx-4 flex flex-col gap-4 py-4"> <div in:fade={{ duration: 500 }} class="mx-4 flex flex-col gap-4 py-4">
<p class="text-sm dark:text-immich-dark-fg"> <p class="text-sm dark:text-immich-dark-fg">
<FormatMessage message={$json('admin.storage_template_more_details')} let:tag let:message> <FormatMessage key="admin.storage_template_more_details" let:tag let:message>
{#if tag === 'template-link'} {#if tag === 'template-link'}
<a <a
href="https://immich.app/docs/administration/storage-template" href="https://immich.app/docs/administration/storage-template"
@ -161,7 +161,7 @@
<p class="text-sm"> <p class="text-sm">
<FormatMessage <FormatMessage
message={$json('admin.storage_template_path_length')} key="admin.storage_template_path_length"
values={{ length: parsedTemplate().length + $user.id.length + 'UPLOAD_LOCATION'.length, limit: 260 }} values={{ length: parsedTemplate().length + $user.id.length + 'UPLOAD_LOCATION'.length, limit: 260 }}
let:message let:message
> >
@ -171,7 +171,7 @@
<p class="text-sm"> <p class="text-sm">
<FormatMessage <FormatMessage
message={$json('admin.storage_template_user_label')} key="admin.storage_template_user_label"
values={{ label: $user.storageLabel || $user.id }} values={{ label: $user.storageLabel || $user.id }}
let:message let:message
> >
@ -229,7 +229,7 @@
<section class="flex flex-col gap-2"> <section class="flex flex-col gap-2">
<p> <p>
<FormatMessage <FormatMessage
message={$json('admin.storage_template_migration_info')} key="admin.storage_template_migration_info"
values={{ job: $t('admin.storage_template_migration_job') }} values={{ job: $t('admin.storage_template_migration_job') }}
let:message let:message
> >

View file

@ -2,13 +2,10 @@ import FormatTagB from '$lib/components/i18n/__test__/format-tag-b.svelte';
import FormatMessage from '$lib/components/i18n/format-message.svelte'; import FormatMessage from '$lib/components/i18n/format-message.svelte';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/svelte'; import { render, screen } from '@testing-library/svelte';
import { init, json, locale, register, waitLocale } from 'svelte-i18n'; import { init, locale, register, waitLocale } from 'svelte-i18n';
import { get } from 'svelte/store';
import { describe } from 'vitest'; import { describe } from 'vitest';
describe('FormatMessage component', () => { describe('FormatMessage component', () => {
let $json: (id: string, locale?: string | undefined) => unknown;
beforeAll(async () => { beforeAll(async () => {
register('en', () => register('en', () =>
Promise.resolve({ Promise.resolve({
@ -21,12 +18,11 @@ describe('FormatMessage component', () => {
await init({ fallbackLocale: 'en' }); await init({ fallbackLocale: 'en' });
await waitLocale('en'); await waitLocale('en');
$json = get(json);
}); });
it('formats a plain text message', () => { it('formats a plain text message', () => {
render(FormatMessage, { render(FormatMessage, {
message: $json('hello'), key: 'hello',
values: { name: 'test' }, values: { name: 'test' },
}); });
expect(screen.getByText('Hello test')).toBeInTheDocument(); expect(screen.getByText('Hello test')).toBeInTheDocument();
@ -34,20 +30,20 @@ describe('FormatMessage component', () => {
it('throws an error when locale is empty', async () => { it('throws an error when locale is empty', async () => {
await locale.set(undefined); await locale.set(undefined);
expect(() => render(FormatMessage, { message: undefined })).toThrowError(); expect(() => render(FormatMessage, { key: '' })).toThrowError();
await locale.set('en'); await locale.set('en');
}); });
it('shows raw message when value is empty', () => { it('shows raw message when value is empty', () => {
render(FormatMessage, { render(FormatMessage, {
message: $json('hello'), key: 'hello',
}); });
expect(screen.getByText('Hello {name}')).toBeInTheDocument(); expect(screen.getByText('Hello {name}')).toBeInTheDocument();
}); });
it('shows message when slot is empty', () => { it('shows message when slot is empty', () => {
render(FormatMessage, { render(FormatMessage, {
message: $json('html'), key: 'html',
values: { name: 'test' }, values: { name: 'test' },
}); });
expect(screen.getByText('Hello test')).toBeInTheDocument(); expect(screen.getByText('Hello test')).toBeInTheDocument();
@ -55,7 +51,7 @@ describe('FormatMessage component', () => {
it('renders a message with html', () => { it('renders a message with html', () => {
const { container } = render(FormatTagB, { const { container } = render(FormatTagB, {
message: $json('html'), key: 'html',
values: { name: 'test' }, values: { name: 'test' },
}); });
expect(container.innerHTML).toBe('Hello <strong>test</strong>'); expect(container.innerHTML).toBe('Hello <strong>test</strong>');
@ -63,7 +59,7 @@ describe('FormatMessage component', () => {
it('renders a message with html and plural', () => { it('renders a message with html and plural', () => {
const { container } = render(FormatTagB, { const { container } = render(FormatTagB, {
message: $json('plural'), key: 'plural',
values: { count: 1 }, values: { count: 1 },
}); });
expect(container.innerHTML).toBe('You have <strong>1 item</strong>'); expect(container.innerHTML).toBe('You have <strong>1 item</strong>');
@ -71,8 +67,13 @@ describe('FormatMessage component', () => {
it('protects agains XSS injection', () => { it('protects agains XSS injection', () => {
render(FormatMessage, { render(FormatMessage, {
message: $json('xss'), key: 'xss',
}); });
expect(screen.getByText('<image/src/onerror=prompt(8)>')).toBeInTheDocument(); expect(screen.getByText('<image/src/onerror=prompt(8)>')).toBeInTheDocument();
}); });
it('displays the message key when not found', () => {
render(FormatMessage, { key: 'invalid.key' });
expect(screen.getByText('invalid.key')).toBeInTheDocument();
});
}); });

View file

@ -2,11 +2,11 @@
import FormatMessage from '../format-message.svelte'; import FormatMessage from '../format-message.svelte';
import type { ComponentProps } from 'svelte'; import type { ComponentProps } from 'svelte';
export let message: unknown; export let key: string;
export let values: ComponentProps<FormatMessage>['values']; export let values: ComponentProps<FormatMessage>['values'];
</script> </script>
<FormatMessage {message} {values} let:tag let:message> <FormatMessage {key} {values} let:tag let:message>
{#if tag === 'b'} {#if tag === 'b'}
<strong>{message}</strong> <strong>{message}</strong>
{/if} {/if}

View file

@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import { IntlMessageFormat, type FormatXMLElementFn, type PrimitiveType } from 'intl-messageformat'; import { IntlMessageFormat, type FormatXMLElementFn, type PrimitiveType } from 'intl-messageformat';
import { TYPE, type MessageFormatElement } from '@formatjs/icu-messageformat-parser'; import { TYPE, type MessageFormatElement } from '@formatjs/icu-messageformat-parser';
import { locale as i18nLocale } from 'svelte-i18n'; import { locale as i18nLocale, json } from 'svelte-i18n';
type InterpolationValues = Record<string, PrimitiveType | FormatXMLElementFn<unknown>>; type InterpolationValues = Record<string, PrimitiveType | FormatXMLElementFn<unknown>>;
export let message: unknown; export let key: string;
export let values: InterpolationValues = {}; export let values: InterpolationValues = {};
const getLocale = (locale?: string | null) => { const getLocale = (locale?: string | null) => {
@ -16,13 +16,13 @@
return locale; return locale;
}; };
const getElements = (message: unknown, locale: string): MessageFormatElement[] => { const getElements = (message: string, locale: string): MessageFormatElement[] => {
return new IntlMessageFormat(message as string, locale, undefined, { return new IntlMessageFormat(message as string, locale, undefined, {
ignoreTag: false, ignoreTag: false,
}).getAst(); }).getAst();
}; };
const getParts = (message: unknown, locale: string) => { const getParts = (message: string, locale: string) => {
try { try {
const elements = getElements(message, locale); const elements = getElements(message, locale);
@ -38,12 +38,13 @@
}); });
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
console.warn(`Message "${message}" has syntax error:`, error.message); console.warn(`Message "${key}" has syntax error:`, error.message);
} }
return [{ message: message as string, tag: undefined }]; return [{ message: message as string, tag: undefined }];
} }
}; };
$: message = ($json(key) as string) || key;
$: locale = getLocale($i18nLocale); $: locale = getLocale($i18nLocale);
$: parts = getParts(message, locale); $: parts = getParts(message, locale);
</script> </script>

View file

@ -9,7 +9,7 @@
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import Icon from '../elements/icon.svelte'; import Icon from '../elements/icon.svelte';
import OnboardingCard from './onboarding-card.svelte'; import OnboardingCard from './onboarding-card.svelte';
import { json, t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import FormatMessage from '$lib/components/i18n/format-message.svelte'; import FormatMessage from '$lib/components/i18n/format-message.svelte';
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
@ -30,7 +30,7 @@
</p> </p>
<p> <p>
<FormatMessage message={$json('admin.storage_template_onboarding_description')} let:message> <FormatMessage key="admin.storage_template_onboarding_description" let:message>
<a class="underline" href="https://immich.app/docs/administration/storage-template">{message}</a> <a class="underline" href="https://immich.app/docs/administration/storage-template">{message}</a>
</FormatMessage> </FormatMessage>
</p> </p>

View file

@ -3,7 +3,7 @@
import type { ServerVersionResponseDto } from '@immich/sdk'; import type { ServerVersionResponseDto } from '@immich/sdk';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import FullScreenModal from './full-screen-modal.svelte'; import FullScreenModal from './full-screen-modal.svelte';
import { json, t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import FormatMessage from '$lib/components/i18n/format-message.svelte'; import FormatMessage from '$lib/components/i18n/format-message.svelte';
let showModal = false; let showModal = false;
@ -37,7 +37,7 @@
{#if showModal} {#if showModal}
<FullScreenModal title="🎉 NEW VERSION AVAILABLE" onClose={() => (showModal = false)}> <FullScreenModal title="🎉 NEW VERSION AVAILABLE" onClose={() => (showModal = false)}>
<div> <div>
<FormatMessage message={$json('version_announcement_message')} let:tag let:message> <FormatMessage key="version_announcement_message" let:tag let:message>
{#if tag === 'link'} {#if tag === 'link'}
<span class="font-medium underline"> <span class="font-medium underline">
<a href="https://github.com/immich-app/immich/releases/latest" target="_blank" rel="noopener noreferrer"> <a href="https://github.com/immich-app/immich/releases/latest" target="_blank" rel="noopener noreferrer">