-
Notifications
You must be signed in to change notification settings - Fork 815
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
Conversation
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.
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 cleaning up the units.
src/server_manager/web_app/ui_components/outline-server-view.js
Outdated
Show resolved
Hide resolved
src/server_manager/web_app/ui_components/outline-server-view.js
Outdated
Show resolved
Hide resolved
src/server_manager/web_app/ui_components/outline-server-view.js
Outdated
Show resolved
Hide resolved
src/server_manager/web_app/ui_components/outline-server-view.js
Outdated
Show resolved
Hide resolved
src/server_manager/web_app/ui_components/outline-server-view.js
Outdated
Show resolved
Hide resolved
This code is only intended to be run in a web environment, not in Node.
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.
The internationalization looks good!
And thanks for fixing the localization and karma tests 🎉
return out; | ||
} | ||
|
||
export function getFormattedDataAmountParts(amount: number, language: string) { |
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.
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.
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.
Done in 49ce60d, let me know if they're useful
}; | ||
} | ||
|
||
export function formatBytes(numBytes: number, language: string) { |
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.
Should we name these two methods something similar to show that they're related?
Maybe like formatBytes
and formatBytesToParts
?
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.
Done in 49ce60d, that's a good suggestion
}, | ||
|
||
_getInternationalizedUnit(bytesAmount, language) { | ||
// This happens during app startup before we set the language |
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'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?
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 was unsure about which defaults to pick. I went with English and the Unix epoch, how do you think about 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.
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 |
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.
Can you add a JS Doc type comment to the constructor instead?
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.
Done in 49ce60d
|
||
describe('formatBytes', () => { | ||
it('Formats data amounts', () => { | ||
expect(i18n.formatBytes(10 * 10 ** 9, 'en')).toEqual('10 GB'); |
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.
Can you add a KB test so that we cover that code path?
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.
Done in 49ce60d
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 don't see it 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.
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
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.
It seems the KB test was added to the formatBytesParts
above.
src/server_manager/web_app/app.ts
Outdated
await this.appRoot.setLanguage(languageCode, languageDir); | ||
document.documentElement.setAttribute('dir', languageDir); | ||
window.localStorage.setItem('overrideLanguage', languageCode); | ||
this.appRoot.showServerView(); |
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 don't think we should call showServerView
here. It might be confusing to the user if the app changes screens.
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 thought I had got rid of that! Done in 49ce60d
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 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. |
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 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?
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 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 |
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.
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.
src/server_manager/web_app/ui_components/outline-server-view.js
Outdated
Show resolved
Hide resolved
|
||
describe('formatBytes', () => { | ||
it('Formats data amounts', () => { | ||
expect(i18n.formatBytes(10 * 10 ** 9, 'en')).toEqual('10 GB'); |
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 don't see it there.
src/server_manager/web_app/ui_components/outline-server-view.js
Outdated
Show resolved
Hide resolved
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 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.
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('۱۳۳٫۵'); | ||
}); | ||
} | ||
}); |
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 looks good. An alternative is to mark the test explicitly as skipped on Node, so it shows on the output:
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
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.
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'); |
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.
It seems the KB test was added to the formatBytesParts
above.
…ues with undefined language at startup
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.
🔥
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.