-
-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(web): translations #9854
feat(web): translations #9854
Conversation
This PR is about the technical aspects of translating the web, so please refrain from commenting about specific translations. Those can be dealt with independently of these changes. (The translation values will be managed through an external platform). With that said, I think this looks really good so far. I wonder what would be the best way to move forward. Presumably we want to share translation strings between mobile and web. Personally, I prefer the flat structure as the hierarchy might become coupled with the implementation (code), which might easily change in the future. Also, I think translating the admin settings are a much lower priority than the sidebar, asset viewer, albums page, and the asset grid component, so maybe we start with those pages. I think it would be good to get a solid foundation and then merge it in, we can incrementally externalize translation strings in follow-up PRs, thus avoiding merge conflicts that inevitably come from long standing PRs. |
web/src/hooks.server.ts
Outdated
import type { Handle } from '@sveltejs/kit' | ||
import { locale } from 'svelte-i18n' | ||
|
||
export const handle: Handle = async ({ event, resolve }) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good default. Using browser locale is a generally close to accurate. However, I do think we should only treat this as a default/fallback. I think there should be a account-setting that allows you to change the language for your user, overriding the browser locale.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I absolutely agree. This is in a early stage and I was just testing around until now so I didn't bother to also look into how I could achieve something like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there should be a account-setting that allows you to change the language
Strong agree, and ideally this setting works for the mobile app as well. @jrasm91 with the new user preferences this should be relatively straightforward right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup.
I did look into that a bit with weblate. AFAIK you can have different file structures for different components. If you want to make it really easy you'd need to define a glossary (weblate can use existing translations as a glossary) and need to use the same keys for the same translations. Unfortunately I haven't found a way to tell weblate to connect a translation to a specific glossary item even if the keys differ or at least mark the translation as "To check" at least. But perhaps I just didn't find it. Most important question how to proceed would be: How should the keys be defined as changing them later on will be a hassle, but I'm afraid this will lead to neverending discussions. |
Yeah I think this is a good point. We should decide on our optimal format and start using that. The mobile app should migrate to it in the future as well. I would prefer having the translations be somewhat independent of the code/UI though. We don't need "app bar" or "bottom sheet" in the key. I think a "cancel" key would probably be fine. |
I changed my approach a bit. Wrote a simple script to extract the strings from the files, automatically create keys and replace the occurences. The automatic creation of the keys I only did for strings of at most 4 words and just replaced whitespaces with underscores and made the string lowercased. So "Add to" has the key "add_to". I added an automatically translated german file for testing purposes. |
Looks good! Massive Update on source Code, Ive only briefly skimmed through it (the individual commits) and everything looks clean so far. PS: the German translation is crappy :D |
A small correction of |
I extracted and replaced some more strings. As these are mostly longer sentences I just used "placeholder_X" for the key for now. I suppose these keys should be decided by you. Big Question: How to proceed. Do you want to merge the currently available stuff? If so, with or without the autotranslated german file? Personally, I wouldn't include it, because.... it's really really crappy and I only changed those which annoyed me the most (translation for cancel for example). |
Currently translated at 100.0% (4 of 4 strings) Translation: immich/web Translate-URL: http://familie-mach.net/projects/immich/web/de/
Currently translated at 100.0% (4 of 4 strings) Translation: immich/web Translate-URL: http://familie-mach.net/projects/immich/web/fr/
Currently translated at 100.0% (18 of 18 strings) Translation: immich/web Translate-URL: http://familie-mach.net/projects/immich/web/de/
I think it looks good so far. We probably could just commit the English values for now and then finish setting up weblate for other languages |
…ew unnecessary replacements
cc @alextran1502, is weblate just ready to go or do we need to prep anything more? |
FYI: I just rebased the branch to the current main branch of immich and also removed the german translation file. |
web/src/lib/i18n/locales/en-EN.json
Outdated
"thirty_minutes": "30 minutes", | ||
"one_hour": "1 hour", | ||
"six_hours": "6 hours", | ||
"one_day": "1 day", | ||
"seven_days": "7 days", | ||
"thirty_days": "30 days", | ||
"three_months": "3 months", | ||
"one_year": "1 year", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For fields like this, should we use plurals? Per https://formatjs.io/docs/core-concepts/icu-syntax/#plural-format (though I may have made a mistake here):
{"hour": "{count, plural, one {count day} other {{count, number} days}"}
And then using that like
{$_('hour', { values: { count: 3 }})}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Plurals are very welcomed, especially for non-germanic languages (e.g. slavic).
web/src/lib/i18n/locales/en-EN.json
Outdated
"thumbnail_resolution": "Thumbnail resolution", | ||
"tone-mapping_npl": "TONE-MAPPING NPL", | ||
"tone-mapping": "TONE-MAPPING", | ||
"transcode_policy": "Transcode policy", | ||
"two-pass_encoding": "TWO-PASS ENCODING", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's some inconsistency in the use of casing here (and of toUpperCase where these values are used)
Those placeholders can be named according previous label... |
I'll go ahead and finish cleaning up the translations and then merge this. Once merged, I'll configure weblate with the git integration and we can go from there. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on this!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for pushing this forward! :)
export const lang = persisted<string | undefined>('lang', undefined, { | ||
serializer: { | ||
parse: (text) => text, | ||
stringify: (object) => object ?? '', | ||
}, | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dedicated preference
@@ -155,7 +157,7 @@ export const locales = [ | |||
{ code: 'en-TT', name: 'English (Trinidad and Tobago)' }, | |||
{ code: 'en-VI', name: 'English (U.S. Virgin Islands)' }, | |||
{ code: 'en-GB', name: 'English (United Kingdom)' }, | |||
{ code: 'en-US', name: 'English (United States)' }, | |||
{ code: 'en-US', name: 'English (United States)', loader: () => import('$lib/i18n/en-US.json') }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Languages are a subset of locales.
export const langs = [ | ||
...locales.filter((item): item is LanguageLoader => !!item.loader), | ||
{ name: 'Development', code: 'dev', loader: () => Promise.resolve({}) }, | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Locales with loaders + development language (shows raw translation keys)
for (const { code, loader } of langs) { | ||
register(code, loader); | ||
} | ||
|
||
const preferenceLang = get(lang); | ||
|
||
await init({ fallbackLocale: fallbackLang, initialLocale: preferenceLang || fallbackLang }); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Load languages and default to preference or english if unset.
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; | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle language change (need to remove the english fallback to see the development keys)
See Discussion #1688
Ignore the hooks.server.ts and the changes to vite.config.js.
The en-EN.json I created manually.
The de-DE.json I created initially manually but it was filled by weblate.
fr.json was created by weblate.
What you see in those files is everything I entered into weblate just to test if it works.
Copy of my comment from the discussion:
So I did invest some time on this and I currently could display quite a few texts in different languages and also make use of weblate.
Disregarding the weblate portion, I want to make sure I'm on the right track about translatability of the web.
This means my file currently would look like this for example:
For components the top level would be "component". For stuff which is used more often like "Video" or "User" there would be a top level "common" and so on. In the svelte files you'd then use
$t(page.admin.user_management.error_create)
for example. For stuff with placeholders you'd use
$t(page.admin.user_management.error_create, {values: {placeholdername: placeholdervalue}}).
With the placeholders you can also use plurals, so instead of having two rules strings for ""Matched 1 item" and "Matched 10 items" there would only be one "ICU plural messages" Matched {count, plural, one {# item} other {# items}}
What do you think? Is this the correct way for you or should I go at it some other way?