Skip to content

Conversation

@katinthehatsite
Copy link
Contributor

@katinthehatsite katinthehatsite commented Apr 23, 2024

Related to https://github.com/Automattic/dotcom-forge/issues/6666

Proposed Changes

This PR makes the demo site expiration date translatable into other languages.

Testing Instructions

Testing for a plural value

  • Pull the changes from this branch locally
  • Before starting the app, modify the language in locale.ts in getPreferredSystemLanguages to return Polish:
export function getPreferredSystemLanguages() {
	if ( process.platform === 'linux' && process.env.NODE_ENV !== 'test' ) {
			.getPreferredSystemLanguages()
			.filter( ( lang ) => supportedLocales.includes( lang ) );
	}
	return [ 'pl' ];
}
  • Open src/translations/local-environment-pl.jed.json and add the following translations to the file:
            "%d hour": [
                "%d godzinę",
                "%d godziny",
                "%d godzin"
            ],
            "%d minute": [
                "%d minutę",
                "%d minuty",
                "%d minut"
            ],
            "%d day": [
                "%d dzień",
                "%d dni",
                "%d dni"
            ]
  • Start the app with nvm use && npm install && npm start (this step is important as we just modified translations)
  • Create a demo site
  • Confirm that instead of days in English, you can see the Polish translation:
Screenshot 2024-04-25 at 2 53 28 PM
  • Close the app completely

Testing for a singular value

  • In src/hooks/use-expiration-date.ts, change the const endDate to the following value:
const endDate = new Date(now.getTime() + (1000 * 60 * 60 * 24) + (1000 * 60 * 20));
  • Start the app with nvm use && npm install && npm start
  • Create a demo site
  • Confirm that instead of day in English, you can see the Polish translation:
Screenshot 2024-04-25 at 2 55 22 PM

Testing for genitive case

  • In src/hooks/use-expiration-date.ts, change the const endDate to the following value:
const endDate = new Date( now.getTime() + 1000 * 60 * 60 * 26 );
  • Start the app with nvm use && npm install && npm start
  • Create a demo site
  • Confirm that instead of day in English, you can see the Polish translation:
Screenshot 2024-04-25 at 2 57 16 PM

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?

@derekblank derekblank self-requested a review April 24, 2024 02:12
@katinthehatsite katinthehatsite requested review from a team and sejas April 24, 2024 11:18
Copy link
Member

@sejas sejas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works as expected.
I edited src/translations/local-environment-es.jed.json and started Studio, and I confirm the formatDifference is translated.

I also investigated if date-fns has already these strings translated but I couldn't find anything. We can pass a function locale.formatDistance, but we will need to add the translations and replace the string anyway. There is an open issue where the solutions are very similar to ours date-fns/date-fns#2134

I just suggested using a for loop instead of a reduce function. But it's not a strong opinion.

Screenshot 2024-04-24 at 12 37 02

Comment on lines 44 to 56
const countDownTranslated = [
{ key: 'days', value: __( 'days' ) },
{ key: 'day', value: __( 'day' ) },
{ key: 'hours', value: __( 'hours' ) },
{ key: 'hour', value: __( 'hour' ) },
{ key: 'minutes', value: __( 'minutes' ) },
{ key: 'minute', value: __( 'minute' ) },
];

// Map the translated values to the countDown string
const translatedCountDown = countDownTranslated.reduce( ( acc, { key, value } ) => {
return acc.replace( key, value );
}, countDown );
Copy link
Member

@sejas sejas Apr 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!, I tested it and it works great.
As a suggestion, here is another alternative that I think is easier to understand:

- const countDown = formatDuration(
+ let countDown = formatDuration(
Suggested change
const countDownTranslated = [
{ key: 'days', value: __( 'days' ) },
{ key: 'day', value: __( 'day' ) },
{ key: 'hours', value: __( 'hours' ) },
{ key: 'hour', value: __( 'hour' ) },
{ key: 'minutes', value: __( 'minutes' ) },
{ key: 'minute', value: __( 'minute' ) },
];
// Map the translated values to the countDown string
const translatedCountDown = countDownTranslated.reduce( ( acc, { key, value } ) => {
return acc.replace( key, value );
}, countDown );
const countDownTranslated = {
days: __( 'days' ),
day: __( 'day' ),
hours: __( 'hours' ),
hour: __( 'hour' ),
minutes: __( 'minutes' ),
minute: __( 'minute' ),
};
// Replace each translated word to the countDown string
for ( const [ key, value ] of Object.entries( countDownTranslated ) ) {
countDown = countDown.replace( key, value );
}
- countDown: isExpired ? __( 'Expired' ) : translatedCountDown,
+ countDown: isExpired ? __( 'Expired' ) : countDown,

@wojtekn
Copy link
Contributor

wojtekn commented Apr 24, 2024

@katinthehatsite I see two possible issues with that approach. First, we don't handle plurals, so we would produce incorrect translations in some cases. E.g. in Polish, if we translate 'hour' to 'godzina' and 'hours' to 'godziny':

  • 1 hour: 1 godzina
  • 2, 3, 4 hours: 2, 3, 4 godziny
  • 5 hours: 5 godziny, but should be 5 godzin

To cover for that, should we translate words with a number and support plurals e.g. _n( '%d hour', '%d hours', hour )?

The second one looks more complex. I'm wondering if there are any languages where translation would use a different order of days/hours/minutes. E.g. hypothetically, if there is a language where '3 days 4 hours' should be translated to '4 hours 3 days'. This might be challenging to solve, though, as we might need to have support for multiple plurals in a sentence.

@sejas
Copy link
Member

sejas commented Apr 24, 2024

To cover for that, should we translate words with a number and support plurals e.g. _n( '%d hour', '%d hours', hour )?

My understanding is that the date-fns library already outputs the "regular" singular and plural.
I agree that to cover the different order that Arabic or Chinese (RTL) could have, we should add the word and number (%d) into the translation string.

The options I see to cover the genitive plural are:

  1. add a "complex" logic
  2. add all the numbers into translation. 1-7 days, 1,23 hours, 1,59 minutes.
  3. A workaround could be translating only the initial or first three letters for those languages that have different endings for plural of 5+.

Maybe there is a better way. My assumption is that _n won't work for that kind of plural.

@katinthehatsite
Copy link
Contributor Author

I was just playing with _n and was not able to make it work for genitive plurals yet. I will take further look at what we can do.

@katinthehatsite
Copy link
Contributor Author

A workaround could be translating only the initial or first three letters for those languages that have different endings for plural of 5+.

I think this might tricky because not all languages have the plural of 5+ that is different for the genitive case. For example, in Irish, there are special cases for 1, 2, 3-6, 7-11 and the rest of the numbers.

@wojtekn
Copy link
Contributor

wojtekn commented Apr 24, 2024

@katinthehatsite @sejas, what if we do the following for now?

if (need_days) {
_n( '%d day', '%d days', day ) + ', ' + _n( '%d hour', '%d hours', hour ) + ', ' + _n( '%d minute', '%d minutes', minute )
} else if (need_hours) {
_n( '%d hour', '%d hours', hour ) + ', ' + _n( '%d minute', '%d minutes', minute )
} else if (need_minutes) {
_n( '%d minute', '%d minutes', minute )
}

@derekblank
Copy link
Contributor

derekblank commented Apr 25, 2024

+1 for continuing to explore a solution using date-fns/locale, even if it's not the short-term solution used in this PR.

It's a bit different than how we're currently handling it, but as you've probably already encountered with date-fns/locale i18n, locales can manually imported as needed and can be passed as options:

import { formatDistance } from "date-fns";
// Require Esperanto locale
import { eo } from "date-fns/locale";

const result = formatDistance(
  new Date(2016, 7, 1),
  new Date(2015, 0, 1),
  { locale: eo }, // Pass the locale as an option
);
//=> 'pli ol 1 jaro'

Some of the locales are quite robust in how they handle declension (depending on how well the locale has been set up). Each locale has an output snapshot where the cases can be reviewed -- check out the Polish locale snapshot, for example.

I understand that we'd already be importing the local-environment-**.jed.json translation files that may already have these words translated to some degree, but building a formatter using intervalToDuration and format from date-fns/locale as @sejas described in #16 (review) may be worth it to handle complex cases (where declensions like genitive plurals have already been handled).

Another simple alternative could be to just say the date: "Expires on January 1st, 2025".

@wojtekn
Copy link
Contributor

wojtekn commented Apr 25, 2024

Another simple alternative could be to just say the date: "Expires on January 1st, 2025".

A simple solution for a complex problem! 🧐

+1 for continuing to explore a solution using date-fns/locale, even if it's not the short-term solution used in this PR.

Wouldn't formatDuration() do exactly what we need?

formatDuration({
  years: 2,
  months: 9,
  weeks: 1,
  days: 7,
  hours: 5,
  minutes: 9,
  seconds: 30
})
//=> '2 years 9 months 1 week 7 days 5 hours 9 minutes 30 seconds'

@katinthehatsite
Copy link
Contributor Author

katinthehatsite commented Apr 25, 2024

Another simple alternative could be to just say the date: "Expires on January 1st, 2025".

I already thought about this 😅 But not sure if we would want to be more precise when the expiration date is less than 24 hours and we have to go for displaying minutes and hours unless we display some sort of timer instead 😅

I am going to try a couple more things based on the suggestions that came in and then we can go from there :)

@sejas
Copy link
Member

sejas commented Apr 25, 2024

Wouldn't formatDuration() do exactly what we need?

Yes! and it's what we are using, but it doesn't come with translations out of the box, but it offers locale.formatDistance as a custom function to add your own translations.
Kat and I checked that _n should handle genitive plurals (TIL), and that's why the translations JSON are arrays of strings instead of directly strings.

const countDown = formatDuration(
intervalToDuration( {
start: now,
// Add 1 hour to the end date serves us here, as for the last hour
// we show the minutes left.
end: addHours( endDate, 1 ),
} ),
{ format, delimiter: ', ' }
);

@katinthehatsite
Copy link
Contributor Author

After some discussions with @sejas (thank you!), I have updated this PR to use formatDistance in the locale that is passed to formatDuration. Let me know what you think!

Alternatively, date-fns (as @derekblank mentioned) has its own locales that already take care of translating all of that. I explored this approach in an alternative PR: #34
Essentially I mapped our own locales to the locales that come from date-fns in that PR.

Both approaches work and we can use either. Personally, I think I prefer the approach in this PR because:

  • we use our own locales that we translate and take care of so we have control over that;
  • if we add more languages in the future, the new locales might not be available in date-fns.

Give it a try and let me know what you think!

cc @wojtekn @derekblank @sejas

Copy link
Contributor

@wojtekn wojtekn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@katinthehatsite, thanks for exploring an approach that handles plurals and for exploring an alternative approach that does that using locales built into the library and creating PR for that.

I reviewed and tested both of them. The above is the clearest one and works better than the other one. For '1 day, 1 hour, ' I've got '1 dzień, 1 godzinę,' which is clear and correct. The built-in locale produced '1 dzień, godzina,' which was not that clear.

Before starting the app, modify the language in locale.ts in getPreferredSystemLanguages to return Polish:

Alternatively, you could also change language in macOS' Language & Settings.

locale: {
formatDistance: ( token, count ) => {
let stringToFormat = '';
switch ( token ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really readable and easy to understand!

@katinthehatsite
Copy link
Contributor Author

I reviewed and tested both of them. The above is the clearest one and works better than the other one. For '1 day, 1 hour, ' I've got '1 dzień, 1 godzinę,' which is clear and correct. The built-in locale produced '1 dzień, godzina,' which was not that clear.

I think it is because _n handles the context better compared to data-fns.

I am going to merge this one and close the other one as it was meant only as exploration 😄

@katinthehatsite katinthehatsite merged commit a8706f8 into trunk Apr 26, 2024
@katinthehatsite katinthehatsite deleted the add/expiration-date-string-translation branch April 26, 2024 08:59
@derekblank
Copy link
Contributor

Kat and I checked that _n should handle genitive plurals (TIL)

Oh cool, TIL as well. Great to see that a solution using _n worked out. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants