feat(web): translations (#9854)

* First test

* Added translation using Weblate (French)

* Translated using Weblate (German)

Currently translated at 100.0% (4 of 4 strings)

Translation: immich/web
Translate-URL: http://familie-mach.net/projects/immich/web/de/

* Translated using Weblate (French)

Currently translated at 100.0% (4 of 4 strings)

Translation: immich/web
Translate-URL: http://familie-mach.net/projects/immich/web/fr/

* Further testing

* Further testing

* Translated using Weblate (German)

Currently translated at 100.0% (18 of 18 strings)

Translation: immich/web
Translate-URL: http://familie-mach.net/projects/immich/web/de/

* Further work

* Update string file.

* More strings

* Automatically changed strings

* Add automatically translated german file for testing purposes

* Fix merge-face-selector component

* Make server stats strings uppercase

* Fix uppercase string

* Fix some strings in jobs-panel

* Fix lower and uppercase strings. Add a few additional string. Fix a few unnecessary replacements

* Update german test translations

* Fix typo in locales file

* Change string keys

* Extract more strings

* Extract and replace some more strings

* Update testtranslationfile

* Change translation keys

* Fix rebase errors

* Fix one more rebase error

* Remove german translation file

* Co-authored-by: Daniel Dietzler <danieldietzler@users.noreply.github.com>

* chore: clean up translations

* chore: add new line

* fix formatting

* chore: fixes

* fix: loading and tests

---------

Co-authored-by: root <root@Blacki>
Co-authored-by: admin <admin@example.com>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
This commit is contained in:
Manic-87 2024-06-04 21:53:00 +02:00 committed by GitHub
parent a2bccf23c9
commit f446bc8caa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
177 changed files with 2779 additions and 1017 deletions

View file

@ -2,10 +2,11 @@
import type { ComboBoxOption } from '$lib/components/shared-components/combobox.svelte';
import SettingCombobox from '$lib/components/shared-components/settings/setting-combobox.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { fallbackLocale, locales } from '$lib/constants';
import { fallbackLang, fallbackLocale, langs, locales } from '$lib/constants';
import {
alwaysLoadOriginalFile,
colorTheme,
lang,
locale,
loopVideo,
playVideoThumbnailOnHover,
@ -15,6 +16,8 @@
import { findLocale } from '$lib/utils';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import { t, locale as i18nLocale, init } from 'svelte-i18n';
import { get } from 'svelte/store';
let time = new Date();
@ -62,6 +65,20 @@
$locale = $locale ? undefined : fallbackLocale.code;
};
const handleLanguageChange = async (newLang: string | undefined) => {
newLang = newLang || fallbackLang;
$lang = newLang;
const previousLang = get(i18nLocale);
if (newLang === 'dev') {
await init({ fallbackLocale: 'dev', initialLocale: 'dev' });
} else if (previousLang == 'dev' && newLang !== 'dev') {
await init({ fallbackLocale: 'en-US', initialLocale: newLang });
}
$i18nLocale = newLang;
};
const handleLocaleChange = (newLocale: string | undefined) => {
$locale = newLocale;
};
@ -72,17 +89,28 @@
<div class="ml-4 mt-4 flex flex-col gap-4">
<div class="ml-4">
<SettingSwitch
title="Theme selection"
subtitle="Automatically set the theme to light or dark based on your browser's system preference"
title={$t('theme_selection')}
subtitle={$t('theme_selection_description')}
bind:checked={$colorTheme.system}
on:toggle={handleToggleColorTheme}
/>
</div>
<div class="ml-4">
<SettingCombobox
comboboxPlaceholder={$t('language')}
{selectedOption}
options={langs.map((lang) => ({ label: lang.name, value: lang.code }))}
title={$t('language')}
subtitle={$t('language_setting_description')}
onSelect={(combobox) => handleLanguageChange(combobox?.value)}
/>
</div>
<div class="ml-4">
<SettingSwitch
title="Default Locale"
subtitle="Format dates and numbers based on your browser locale"
title={$t('default_locale')}
subtitle={$t('default_locale_description')}
checked={$locale == undefined}
on:toggle={handleToggleLocaleBrowser}
>
@ -92,11 +120,11 @@
{#if $locale !== undefined}
<div class="ml-4">
<SettingCombobox
comboboxPlaceholder="Searching locales..."
comboboxPlaceholder={$t('searching_locales')}
{selectedOption}
options={getAllLanguages()}
title="Custom Locale"
subtitle="Format dates and numbers based on the language and the region"
title={$t('custom_locale')}
subtitle={$t('custom_locale_description')}
onSelect={(combobox) => handleLocaleChange(combobox?.value)}
/>
</div>
@ -104,8 +132,8 @@
<div class="ml-4">
<SettingSwitch
title="Display original photos"
subtitle="Prefer to display the original photo when viewing an asset rather than thumbnails when the original asset is web-compatible. This may result in slower photo display speeds."
title={$t('display_original_photos')}
subtitle={$t('display_original_photos_setting_description')}
bind:checked={$alwaysLoadOriginalFile}
on:toggle={() => ($alwaysLoadOriginalFile = !$alwaysLoadOriginalFile)}
/>
@ -113,15 +141,15 @@
<div class="ml-4">
<SettingSwitch
title="Play video thumbnail on hover"
subtitle="Play video thumbnail when mouse is hovering over item. Even when disabled, playback can be started by hovering over the play icon."
subtitle={$t('video_hover_setting_description')}
bind:checked={$playVideoThumbnailOnHover}
on:toggle={() => ($playVideoThumbnailOnHover = !$playVideoThumbnailOnHover)}
/>
</div>
<div class="ml-4">
<SettingSwitch
title="Loop videos"
subtitle="Enable to automatically loop a video in the detail viewer."
title={$t('loop_videos')}
subtitle={$t('loop_videos_description')}
bind:checked={$loopVideo}
on:toggle={() => ($loopVideo = !$loopVideo)}
/>
@ -129,23 +157,23 @@
<div class="ml-4">
<SettingSwitch
title="Permanent deletion warning"
subtitle="Show a warning when permanently deleting assets"
title={$t('permanent_deletion_warning')}
subtitle={$t('permanent_deletion_warning_setting_description')}
bind:checked={$showDeleteModal}
/>
</div>
<div class="ml-4">
<SettingSwitch
title="People"
subtitle="Display a link to People in the sidebar"
title={$t('people')}
subtitle={$t('people_sidebar_description')}
bind:checked={$sidebarSettings.people}
/>
</div>
<div class="ml-4">
<SettingSwitch
title="Sharing"
subtitle="Display a link to Sharing in the sidebar"
title={$t('sharing')}
subtitle={$t('sharing_sidebar_description')}
bind:checked={$sidebarSettings.sharing}
/>
</div>

View file

@ -11,6 +11,7 @@
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import { t } from 'svelte-i18n';
let password = '';
let newPassword = '';
@ -21,7 +22,7 @@
await changePassword({ changePasswordDto: { password, newPassword } });
notificationController.show({
message: 'Updated password',
message: $t('updated_password'),
type: NotificationType.Info,
});
@ -44,7 +45,7 @@
<div class="ml-4 mt-4 flex flex-col gap-4">
<SettingInputField
inputType={SettingInputFieldType.PASSWORD}
label="PASSWORD"
label={$t('password').toUpperCase()}
bind:value={password}
required={true}
passwordAutocomplete="current-password"
@ -52,7 +53,7 @@
<SettingInputField
inputType={SettingInputFieldType.PASSWORD}
label="NEW PASSWORD"
label={$t('new_password').toUpperCase()}
bind:value={newPassword}
required={true}
passwordAutocomplete="new-password"
@ -60,7 +61,7 @@
<SettingInputField
inputType={SettingInputFieldType.PASSWORD}
label="CONFIRM PASSWORD"
label={$t('confirm_password').toUpperCase()}
bind:value={confirmPassword}
required={true}
passwordAutocomplete="new-password"
@ -71,7 +72,7 @@
type="submit"
size="sm"
disabled={!(password && newPassword && newPassword === confirmPassword)}
on:click={() => handleChangePassword()}>Save</Button
on:click={() => handleChangePassword()}>{$t('save')}</Button
>
</div>
</div>

View file

@ -15,6 +15,7 @@
} from '@mdi/js';
import { DateTime, type ToRelativeCalendarOptions } from 'luxon';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n';
export let device: SessionResponseDto;
@ -52,11 +53,11 @@
{#if device.deviceType || device.deviceOS}
<span>{device.deviceOS || 'Unknown'}{device.deviceType || 'Unknown'}</span>
{:else}
<span>Unknown</span>
<span>{$t('unknown')}</span>
{/if}
</span>
<div class="text-sm">
<span class="">Last seen</span>
<span class="">{$t('last_seen')}</span>
<span>{DateTime.fromISO(device.updatedAt, { locale: $locale }).toRelativeCalendar(options)}</span>
<span class="text-xs text-gray-500 dark:text-gray-400"> - </span>
<span class="text-xs text-gray-500 dark:text-gray-400">
@ -69,7 +70,7 @@
<CircleIconButton
color="primary"
icon={mdiTrashCanOutline}
title="Log out"
title={$t('log_out')}
size="16"
on:click={() => dispatcher('delete')}
/>

View file

@ -5,6 +5,7 @@
import { notificationController, NotificationType } from '../shared-components/notification/notification';
import DeviceCard from './device-card.svelte';
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
import { t } from 'svelte-i18n';
export let devices: SessionResponseDto[];
@ -60,13 +61,17 @@
<section class="my-4">
{#if currentDevice}
<div class="mb-6">
<h3 class="mb-2 text-xs font-medium text-immich-primary dark:text-immich-dark-primary">CURRENT DEVICE</h3>
<h3 class="mb-2 text-xs font-medium text-immich-primary dark:text-immich-dark-primary">
{$t('current_device').toUpperCase()}
</h3>
<DeviceCard device={currentDevice} />
</div>
{/if}
{#if otherDevices.length > 0}
<div class="mb-6">
<h3 class="mb-2 text-xs font-medium text-immich-primary dark:text-immich-dark-primary">OTHER DEVICES</h3>
<h3 class="mb-2 text-xs font-medium text-immich-primary dark:text-immich-dark-primary">
{$t('other_devices').toUpperCase()}
</h3>
{#each otherDevices as device, index}
<DeviceCard {device} on:delete={() => handleDelete(device)} />
{#if index !== otherDevices.length - 1}
@ -74,9 +79,11 @@
{/if}
{/each}
</div>
<h3 class="mb-2 text-xs font-medium text-immich-primary dark:text-immich-dark-primary">LOG OUT ALL DEVICES</h3>
<h3 class="mb-2 text-xs font-medium text-immich-primary dark:text-immich-dark-primary">
{$t('log_out_all_devices').toUpperCase()}
</h3>
<div class="flex justify-end">
<Button color="red" size="sm" on:click={handleDeleteAll}>Log Out All Devices</Button>
<Button color="red" size="sm" on:click={handleDeleteAll}>{$t('log_out_all_devices')}</Button>
</div>
{/if}
</section>

View file

@ -10,6 +10,7 @@
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { preferences } from '$lib/stores/user.store';
import Button from '../elements/buttons/button.svelte';
import { t } from 'svelte-i18n';
let memoriesEnabled = $preferences?.memories?.enabled ?? false;
@ -18,9 +19,9 @@
const data = await updateMyPreferences({ userPreferencesUpdateDto: { memories: { enabled: memoriesEnabled } } });
$preferences.memories.enabled = data.memories.enabled;
notificationController.show({ message: 'Saved settings', type: NotificationType.Info });
notificationController.show({ message: $t('saved_settings'), type: NotificationType.Info });
} catch (error) {
handleError(error, 'Unable to update settings');
handleError(error, $t('errors.unable_to_update_settings'));
}
};
</script>
@ -31,13 +32,13 @@
<div class="ml-4 mt-4 flex flex-col gap-4">
<div class="ml-4">
<SettingSwitch
title="Time-based memories"
subtitle="Photos from previous years"
title={$t('time_based_memories')}
subtitle={$t('photos_from_previous_years')}
bind:checked={memoriesEnabled}
/>
</div>
<div class="flex justify-end">
<Button type="submit" size="sm" on:click={() => handleSave()}>Save</Button>
<Button type="submit" size="sm" on:click={() => handleSave()}>{$t('save')}</Button>
</div>
</div>
</form>

View file

@ -9,6 +9,7 @@
import Button from '../elements/buttons/button.svelte';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { notificationController, NotificationType } from '../shared-components/notification/notification';
import { t } from 'svelte-i18n';
export let user: UserAdminResponseDto;
@ -22,7 +23,7 @@
user = await oauth.link(window.location);
notificationController.show({
message: 'Linked OAuth account',
message: $t('linked_oauth_account'),
type: NotificationType.Info,
});
} catch (error) {
@ -39,11 +40,11 @@
try {
user = await oauth.unlink();
notificationController.show({
message: 'Unlinked OAuth account',
message: $t('unlinked_oauth_account'),
type: NotificationType.Info,
});
} catch (error) {
handleError(error, 'Unable to unlink account');
handleError(error, $t('errors.unable_to_unlink_account'));
}
};
</script>
@ -57,9 +58,9 @@
</div>
{:else if $featureFlags.oauth}
{#if user.oauthId}
<Button size="sm" on:click={() => handleUnlink()}>Unlink Oauth</Button>
<Button size="sm" on:click={() => handleUnlink()}>{$t('unlink_oauth')}</Button>
{:else}
<Button size="sm" on:click={() => oauth.authorize(window.location)}>Link to OAuth</Button>
<Button size="sm" on:click={() => oauth.authorize(window.location)}>{$t('link_to_oauth')}</Button>
{/if}
{/if}
</div>

View file

@ -4,6 +4,7 @@
import Button from '../elements/buttons/button.svelte';
import UserAvatar from '../shared-components/user-avatar.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import { t } from 'svelte-i18n';
export let user: UserResponseDto;
export let onClose: () => void;
@ -32,7 +33,7 @@
};
</script>
<FullScreenModal title="Add partner" showLogo {onClose}>
<FullScreenModal title={$t('add_partner')} showLogo {onClose}>
<div class="immich-scrollbar max-h-[300px] overflow-y-auto">
{#if availableUsers.length > 0}
{#each availableUsers as user}
@ -68,7 +69,7 @@
{#if selectedUsers.length > 0}
<div class="pt-5">
<Button size="sm" fullwidth on:click={() => dispatch('add-users', selectedUsers)}>Add</Button>
<Button size="sm" fullwidth on:click={() => dispatch('add-users', selectedUsers)}>{$t('add')}</Button>
</div>
{/if}
</div>

View file

@ -17,6 +17,7 @@
import PartnerSelectionModal from './partner-selection-modal.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
import { t } from 'svelte-i18n';
interface PartnerSharing {
user: UserResponseDto;
@ -90,7 +91,7 @@
await removePartner({ id: partner.id });
await refreshPartners();
} catch (error) {
handleError(error, 'Unable to remove partner');
handleError(error, $t('errors.unable_to_remove_partner'));
}
};
@ -103,7 +104,7 @@
await refreshPartners();
createPartnerFlag = false;
} catch (error) {
handleError(error, 'Unable to add partners');
handleError(error, $t('errors.unable_to_add_partners'));
}
};
@ -167,8 +168,8 @@
<hr class="my-4 border border-gray-200 dark:border-gray-700" />
<p class="text-xs font-medium my-4">PHOTOS FROM {partner.user.name.toUpperCase()}</p>
<SettingSwitch
title="Show in timeline"
subtitle="Show photos and videos from this user in your timeline"
title={$t('show_in_timeline')}
subtitle={$t('show_in_timeline_setting_description')}
bind:checked={partner.inTimeline}
on:toggle={({ detail }) => handleShowOnTimelineChanged(partner, detail)}
/>
@ -179,7 +180,7 @@
{/if}
<div class="flex justify-end mt-5">
<Button size="sm" on:click={() => (createPartnerFlag = true)}>Add partner</Button>
<Button size="sm" on:click={() => (createPartnerFlag = true)}>{$t('add_partner')}</Button>
</div>
</section>

View file

@ -10,6 +10,7 @@
import { NotificationType, notificationController } from '../shared-components/notification/notification';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
import { t } from 'svelte-i18n';
export let keys: ApiKeyResponseDto[];
@ -84,8 +85,8 @@
{#if newKey}
<APIKeyForm
title="New API key"
submitText="Create"
title={$t('new_api_key')}
submitText={$t('create')}
apiKey={newKey}
on:submit={({ detail }) => handleCreate(detail)}
on:cancel={() => (newKey = null)}
@ -98,8 +99,8 @@
{#if editKey}
<APIKeyForm
title="API key"
submitText="Save"
title={$t('api_key')}
submitText={$t('save')}
apiKey={editKey}
on:submit={({ detail }) => handleUpdate(detail)}
on:cancel={() => (editKey = null)}
@ -109,7 +110,7 @@
<section class="my-4">
<div class="flex flex-col gap-2" in:fade={{ duration: 500 }}>
<div class="mb-2 flex justify-end">
<Button size="sm" on:click={() => (newKey = { name: 'API Key' })}>New API Key</Button>
<Button size="sm" on:click={() => (newKey = { name: $t('api_key') })}>{$t('new_api_key')}</Button>
</div>
{#if keys.length > 0}
@ -118,9 +119,9 @@
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
>
<tr class="flex w-full place-items-center">
<th class="w-1/3 text-center text-sm font-medium">Name</th>
<th class="w-1/3 text-center text-sm font-medium">Created</th>
<th class="w-1/3 text-center text-sm font-medium">Action</th>
<th class="w-1/3 text-center text-sm font-medium">{$t('name')}</th>
<th class="w-1/3 text-center text-sm font-medium">{$t('created')}</th>
<th class="w-1/3 text-center text-sm font-medium">{$t('action')}</th>
</tr>
</thead>
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
@ -141,14 +142,14 @@
<CircleIconButton
color="primary"
icon={mdiPencilOutline}
title="Edit key"
title={$t('edit_key')}
size="16"
on:click={() => (editKey = key)}
/>
<CircleIconButton
color="primary"
icon={mdiTrashCanOutline}
title="Delete key"
title={$t('delete_key')}
size="16"
on:click={() => handleDelete(key)}
/>

View file

@ -12,6 +12,7 @@
import { fade } from 'svelte/transition';
import { handleError } from '../../utils/handle-error';
import Button from '../elements/buttons/button.svelte';
import { t } from 'svelte-i18n';
let editedUser = cloneDeep($user);
@ -28,11 +29,11 @@
$user = data;
notificationController.show({
message: 'Saved profile',
message: $t('saved_profile'),
type: NotificationType.Info,
});
} catch (error) {
handleError(error, 'Unable to save profile');
handleError(error, $t('errors.unable_to_save_profile'));
}
};
</script>
@ -43,30 +44,34 @@
<div class="ml-4 mt-4 flex flex-col gap-4">
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="USER ID"
label={$t('user_id').toUpperCase()}
bind:value={editedUser.id}
disabled={true}
/>
<SettingInputField inputType={SettingInputFieldType.EMAIL} label="EMAIL" bind:value={editedUser.email} />
<SettingInputField
inputType={SettingInputFieldType.EMAIL}
label={$t('email').toUpperCase()}
bind:value={editedUser.email}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="NAME"
label={$t('name').toUpperCase()}
bind:value={editedUser.name}
required={true}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="STORAGE LABEL"
label={$t('storage_label').toUpperCase()}
disabled={true}
value={editedUser.storageLabel || ''}
required={false}
/>
<div class="flex justify-end">
<Button type="submit" size="sm" on:click={() => handleSaveProfile()}>Save</Button>
<Button type="submit" size="sm" on:click={() => handleSaveProfile()}>{$t('save')}</Button>
</div>
</div>
</form>

View file

@ -16,6 +16,7 @@
import UserAPIKeyList from './user-api-key-list.svelte';
import UserProfileSettings from './user-profile-settings.svelte';
import NotificationsSettings from '$lib/components/user-settings-page/notifications-settings.svelte';
import { t } from 'svelte-i18n';
export let keys: ApiKeyResponseDto[] = [];
export let sessions: SessionResponseDto[] = [];
@ -26,23 +27,23 @@
</script>
<SettingAccordionState queryParam={QueryParameter.IS_OPEN}>
<SettingAccordion key="app-settings" title="App Settings" subtitle="Manage the app settings">
<SettingAccordion key="app-settings" title={$t('app_settings')} subtitle={$t('manage_the_app_settings')}>
<AppSettings />
</SettingAccordion>
<SettingAccordion key="account" title="Account" subtitle="Manage your account">
<SettingAccordion key="account" title={$t('account')} subtitle={$t('manage_your_account')}>
<UserProfileSettings />
</SettingAccordion>
<SettingAccordion key="api-keys" title="API Keys" subtitle="Manage your API keys">
<SettingAccordion key="api-keys" title={$t('api_keys')} subtitle={$t('manage_your_api_keys')}>
<UserAPIKeyList bind:keys />
</SettingAccordion>
<SettingAccordion key="authorized-devices" title="Authorized Devices" subtitle="Manage your logged-in devices">
<SettingAccordion key="authorized-devices" title={$t('authorized_devices')} subtitle={$t('manage_your_devices')}>
<DeviceList bind:devices={sessions} />
</SettingAccordion>
<SettingAccordion key="memories" title="Memories" subtitle="Manage what you see in your memories">
<SettingAccordion key="memories" title={$t('memories')} subtitle={$t('memories_setting_description')}>
<MemoriesSettings />
</SettingAccordion>
@ -51,16 +52,21 @@
</SettingAccordion>
{#if $featureFlags.loaded && $featureFlags.oauth}
<SettingAccordion key="oauth" title="OAuth" subtitle="Manage your OAuth connection" isOpen={oauthOpen || undefined}>
<SettingAccordion
key="oauth"
title={$t('oauth')}
subtitle={$t('manage_your_oauth_connection')}
isOpen={oauthOpen || undefined}
>
<OAuthSettings user={$user} />
</SettingAccordion>
{/if}
<SettingAccordion key="password" title="Password" subtitle="Change your password">
<SettingAccordion key="password" title={$t('password')} subtitle={$t('change_your_password')}>
<ChangePasswordSettings />
</SettingAccordion>
<SettingAccordion key="partner-sharing" title="Partner Sharing" subtitle="Manage sharing with partners">
<SettingAccordion key="partner-sharing" title={$t('partner_sharing')} subtitle={$t('manage_sharing_with_partners')}>
<PartnerSettings user={$user} />
</SettingAccordion>
</SettingAccordionState>