Skip to content

Internationalize units and currency #780

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

Merged
merged 16 commits into from
Feb 4, 2021
Merged

Conversation

JonathanDCohen
Copy link
Contributor

@JonathanDCohen JonathanDCohen commented Dec 17, 2020

Demo video

This required updating to Typescript 4 and adding compiler changes for the server manager in order to support newer features of Intl.

This change also fixes it so that the server city, creation date, cost, server data cap, data limits, etc are no longer stuck to the old language after changing languages. Some of these fixes only work the first time you switch languages, but not if you switch again. I have no idea why that is, but quitting and restarting the app still gets everything back in sync.

This change un-broke the web app tests by correctly building the digitalocean install script to avoid import errors.

Jonathan Cohen added 2 commits December 16, 2020 21:23
This required updating to Typescript 4 and adding compiler changes for the server manager in order to support newer features of `Intl`.

This change also fixes it so that the server city, creation date, and data limits no longer are stuck to the old language ater changing languages.
Copy link
Collaborator

@fortuna fortuna left a comment

Choose a reason for hiding this comment

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

Thanks for cleaning up the units.

Copy link
Contributor

@mpmcroy mpmcroy left a comment

Choose a reason for hiding this comment

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

The internationalization looks good!

And thanks for fixing the localization and karma tests 🎉

return out;
}

export function getFormattedDataAmountParts(amount: number, language: string) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you add return types and JS docs for the exported methods?

As a user, I'd be interested to know which one to choose based on my use case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 49ce60d, let me know if they're useful

};
}

export function formatBytes(numBytes: number, language: string) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we name these two methods something similar to show that they're related?

Maybe like formatBytes and formatBytesToParts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 49ce60d, that's a good suggestion

},

_getInternationalizedUnit(bytesAmount, language) {
// This happens during app startup before we set the language
Copy link
Contributor

Choose a reason for hiding this comment

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

I've run into these kind of issues a few times too.

My approach has been to try to set a sensible default and always maintain an invariant that the value is not null. That allowed me to remove the null|undefined checks.

What are your thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was unsure about which defaults to pick. I went with English and the Unix epoch, how do you think about that?

Copy link
Contributor

Choose a reason for hiding this comment

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

That seems like a good default to me. I suspect the user will not even see the default value since it's not the default tab.

@@ -596,7 +588,7 @@ export class ServerView extends DirMixin(PolymerElement) {
serverManagementApiUrl: String,
serverPortForNewAccessKeys: Number,
isAccessKeyPortEditable: {type: Boolean},
serverCreationDate: String,
serverCreationDate: {type: Object, value: null}, // type: Date
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a JS Doc type comment to the constructor instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 49ce60d


describe('formatBytes', () => {
it('Formats data amounts', () => {
expect(i18n.formatBytes(10 * 10 ** 9, 'en')).toEqual('10 GB');
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a KB test so that we cover that code path?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 49ce60d

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't see it there.

Copy link
Contributor Author

@JonathanDCohen JonathanDCohen Jan 22, 2021

Choose a reason for hiding this comment

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

It's in https://github.com/Jigsaw-Code/outline-server/pull/780/commits/49ce60d8d96a8d1d434992c60e01f04c2a37af65#diff-858c37f4a836374e98fb54a760e5a4099b33a6ae04b41c2079b60c588eeb492cR31, I'm not sure why the view isn't showing it

Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems the KB test was added to the formatBytesParts above.

await this.appRoot.setLanguage(languageCode, languageDir);
document.documentElement.setAttribute('dir', languageDir);
window.localStorage.setItem('overrideLanguage', languageCode);
this.appRoot.showServerView();
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we should call showServerView here. It might be confusing to the user if the app changes screens.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought I had got rid of that! Done in 49ce60d

Copy link
Contributor

@mpmcroy mpmcroy left a comment

Choose a reason for hiding this comment

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

I didn't take a look at the Typescript 4/ES 2020 part since I think @fortuna is better suited to review that, but the rest looks 🔥 🔥 🔥

LGTM!


_getTranslatedDate(language, date) {
// We can't use a default Date object -- it still shows up as null, I'm not sure the lifetime
// properties when you pass an object into Polymer.
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if this happens because we set the default value in the Polymer property configuration. I think this is one of the only files that sets defaults that way.

Does it work if you set serverCreationDate in the constructor?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would guess that it does, but I don't want to change this file to use a constructor in this PR, it's too much going on already and it's not important enough to keep delaying the per key data limits ui for

},

_getInternationalizedUnit(bytesAmount, language) {
// This happens during app startup before we set the language
Copy link
Contributor

Choose a reason for hiding this comment

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

That seems like a good default to me. I suspect the user will not even see the default value since it's not the default tab.


describe('formatBytes', () => {
it('Formats data amounts', () => {
expect(i18n.formatBytes(10 * 10 ** 9, 'en')).toEqual('10 GB');
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't see it there.

Copy link
Collaborator

@fortuna fortuna left a comment

Choose a reason for hiding this comment

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

Thanks for making this change. Our unit formatting is a lot more robust now, thanks to you.

Please make sure the test is passing before submitting. It seems to be broken.

Comment on lines 20 to 47
if (process?.versions?.node) {
it('doesn\'t run on Node', () => {
expect(() => i18n.formatBytesParts(0, 'en')).toThrow();
});
} else {
it('extracts the unit string and value separately', () => {
const english = i18n.formatBytesParts(0, 'en');
expect(english.unit).toEqual('B');
expect(english.value).toEqual('0');

const spanish = i18n.formatBytesParts(2, 'es');
expect(spanish.unit).toEqual('B');
expect(spanish.value).toEqual('2');

const russian = i18n.formatBytesParts(3000, 'ru');
expect(russian.unit).toEqual('кБ');
expect(russian.value).toEqual('3');

const french = i18n.formatBytesParts(1.5 * 10 ** 9, 'fr');
expect(french.unit).toEqual('Go');
expect(french.value).toEqual('1,5');

const farsi = i18n.formatBytesParts(133.5 * 10 ** 6, 'fa');
expect(farsi.unit).toEqual('مگابایت');
expect(farsi.value).toEqual('۱۳۳٫۵');
});
}
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks good. An alternative is to mark the test explicitly as skipped on Node, so it shows on the output:

Suggested change
if (process?.versions?.node) {
it('doesn\'t run on Node', () => {
expect(() => i18n.formatBytesParts(0, 'en')).toThrow();
});
} else {
it('extracts the unit string and value separately', () => {
const english = i18n.formatBytesParts(0, 'en');
expect(english.unit).toEqual('B');
expect(english.value).toEqual('0');
const spanish = i18n.formatBytesParts(2, 'es');
expect(spanish.unit).toEqual('B');
expect(spanish.value).toEqual('2');
const russian = i18n.formatBytesParts(3000, 'ru');
expect(russian.unit).toEqual('кБ');
expect(russian.value).toEqual('3');
const french = i18n.formatBytesParts(1.5 * 10 ** 9, 'fr');
expect(french.unit).toEqual('Go');
expect(french.value).toEqual('1,5');
const farsi = i18n.formatBytesParts(133.5 * 10 ** 6, 'fa');
expect(farsi.unit).toEqual('مگابایت');
expect(farsi.value).toEqual('۱۳۳٫۵');
});
}
});
let itBrowser = it;
if (process?.versions?.node) {
itBrowser = xit;
}
itBrowser('extracts the unit string and value separately', () => {
const english = i18n.formatBytesParts(0, 'en');
expect(english.unit).toEqual('B');
expect(english.value).toEqual('0');
const spanish = i18n.formatBytesParts(2, 'es');
expect(spanish.unit).toEqual('B');
expect(spanish.value).toEqual('2');
const russian = i18n.formatBytesParts(3000, 'ru');
expect(russian.unit).toEqual('кБ');
expect(russian.value).toEqual('3');
const french = i18n.formatBytesParts(1.5 * 10 ** 9, 'fr');
expect(french.unit).toEqual('Go');
expect(french.value).toEqual('1,5');
const farsi = i18n.formatBytesParts(133.5 * 10 ** 6, 'fa');
expect(farsi.unit).toEqual('مگابایت');
expect(farsi.value).toEqual('۱۳۳٫۵');
});
});

Up to you

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a cool idea, but I'm going to keep it the way I wrote it because it tests the runtime check which keeps us from putting the code in Node


describe('formatBytes', () => {
it('Formats data amounts', () => {
expect(i18n.formatBytes(10 * 10 ** 9, 'en')).toEqual('10 GB');
Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems the KB test was added to the formatBytesParts above.

Copy link
Contributor

@mpmcroy mpmcroy left a comment

Choose a reason for hiding this comment

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

🔥

@JonathanDCohen JonathanDCohen merged commit 1c5b563 into master Feb 4, 2021
@JonathanDCohen JonathanDCohen deleted the cohenjon-intl-units branch February 4, 2021 23:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants