@colisweb/rescript-toolkit 2.43.0 → 2.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bsconfig.json +0 -2
- package/locale/fr.json +102 -0
- package/package.json +14 -12
- package/src/Toolkit.res +2 -0
- package/src/form/Toolkit__Form.res +2 -6
- package/src/form/Toolkit__FormValidationFunctions.res +11 -9
- package/src/intl/Toolkit__Intl.res +101 -0
- package/src/intl/Toolkit__Intl.resi +33 -0
- package/src/intl/Toolkit__LocalesHelpers.res +35 -0
- package/src/intl/check.js +20 -19
- package/src/intl/defaultLanguageConverter.js +37 -0
- package/src/intl/extract.js +24 -13
- package/src/ui/Toolkit__Ui_ButtonGroup.res +1 -31
- package/src/ui/Toolkit__Ui_Card.res +2 -8
- package/src/ui/Toolkit__Ui_Checkbox.res +4 -18
- package/src/ui/Toolkit__Ui_Modal.res +22 -31
- package/src/ui/Toolkit__Ui_ProgressBar.res +1 -1
- package/src/ui/Toolkit__Ui_Radio.res +3 -17
- package/src/ui/Toolkit__Ui_Tooltip.res +18 -27
- package/src/ui/styles.css +18 -1
- package/src/vendors/Browser.res +16 -0
- package/src/vendors/Browser.resi +11 -0
- package/src/vendors/ShallowEqual.res +5 -0
- package/src/vendors/reach-ui/ReachUi_Tooltip.res +1 -0
package/bsconfig.json
CHANGED
package/locale/fr.json
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "_05a04636",
|
|
4
|
+
"defaultMessage": "Bin packing",
|
|
5
|
+
"message": "Bin packing"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"id": "_09d99cb1",
|
|
9
|
+
"defaultMessage": "Mardi",
|
|
10
|
+
"message": "Mardi"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"id": "_1484eaf0",
|
|
14
|
+
"defaultMessage": "Format requis : 14 chiffres",
|
|
15
|
+
"message": "Format requis : 14 chiffres"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "_24a9ec30",
|
|
19
|
+
"defaultMessage": "Format requis : 4 caractères ou nombres (exemples : 'A1b2' 'abcd')",
|
|
20
|
+
"message": "Format requis : 4 caractères ou nombres (exemples : 'A1b2' 'abcd')"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "_29102a96",
|
|
24
|
+
"defaultMessage": "Volume total",
|
|
25
|
+
"message": "Volume total"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "_337be526",
|
|
29
|
+
"defaultMessage": "Jeudi",
|
|
30
|
+
"message": "Jeudi"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "_49a79a00",
|
|
34
|
+
"defaultMessage": "Samedi",
|
|
35
|
+
"message": "Samedi"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "_5689ac4d",
|
|
39
|
+
"defaultMessage": "Masquer le mot de passe",
|
|
40
|
+
"message": "Masquer le mot de passe"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "_8f8eb0df",
|
|
44
|
+
"defaultMessage": "Vendredi",
|
|
45
|
+
"message": "Vendredi"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "_90cfad83",
|
|
49
|
+
"defaultMessage": "Mercredi",
|
|
50
|
+
"message": "Mercredi"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "_9fc912ee",
|
|
54
|
+
"defaultMessage": "Lundi",
|
|
55
|
+
"message": "Lundi"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "_aaa4996e",
|
|
59
|
+
"defaultMessage": "Dimanche",
|
|
60
|
+
"message": "Dimanche"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "_c05fffa7",
|
|
64
|
+
"defaultMessage": "Afficher le mot de passe",
|
|
65
|
+
"message": "Afficher le mot de passe"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "_c98895d1",
|
|
69
|
+
"defaultMessage": "Doit etre un entier positif",
|
|
70
|
+
"message": "Doit etre un entier positif"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "_d2c9771a",
|
|
74
|
+
"defaultMessage": "Mauvais format (req: {exemple})",
|
|
75
|
+
"message": "Mauvais format (req: {exemple})"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "_d56f025d",
|
|
79
|
+
"defaultMessage": "Champs requis",
|
|
80
|
+
"message": "Champs requis"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"id": "_d7f4a16b",
|
|
84
|
+
"defaultMessage": "Ce nom est deja utilisé dans un autre forfait",
|
|
85
|
+
"message": "Ce nom est deja utilisé dans un autre forfait"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "_dd093040",
|
|
89
|
+
"defaultMessage": "Doit être un entier positif ou un nombre a virgule",
|
|
90
|
+
"message": "Doit être un entier positif ou un nombre a virgule"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"id": "_e6b5a42a",
|
|
94
|
+
"defaultMessage": "Nbr colis",
|
|
95
|
+
"message": "Nbr colis"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"id": "clipboard.copied",
|
|
99
|
+
"defaultMessage": "Copied to clipboard",
|
|
100
|
+
"message": "Copied to clipboard"
|
|
101
|
+
}
|
|
102
|
+
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colisweb/rescript-toolkit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.46.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"clean": "rescript clean",
|
|
6
6
|
"build": "rescript build -with-deps",
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
"build:reacticons": "node tools/extract-react-icons.js",
|
|
10
10
|
"build:scss": "node tools/build-scss.js",
|
|
11
11
|
"storybook": "STORYBOOK=true start-storybook -p 6006 --no-manager-cache",
|
|
12
|
-
"build-storybook": "TAILWIND_MODE=build STORYBOOK=true build-storybook"
|
|
12
|
+
"build-storybook": "TAILWIND_MODE=build STORYBOOK=true build-storybook",
|
|
13
|
+
"intl:check": "node src/intl/check.js",
|
|
14
|
+
"intl:extract": "node src/intl/extract.js"
|
|
13
15
|
},
|
|
14
16
|
"keywords": [
|
|
15
17
|
"ReScript",
|
|
@@ -21,8 +23,10 @@
|
|
|
21
23
|
"author": "Colisweb",
|
|
22
24
|
"license": "MIT",
|
|
23
25
|
"dependencies": {
|
|
26
|
+
"@colisweb/bs-react-intl-extractor-bin": "0.12.2",
|
|
24
27
|
"@colisweb/react-day-picker": "7.4.16",
|
|
25
28
|
"@colisweb/restorative": "0.5.1",
|
|
29
|
+
"@datadog/browser-rum": "4.14.0",
|
|
26
30
|
"@reach/accordion": "0.17.0",
|
|
27
31
|
"@reach/alert-dialog": "0.17.0",
|
|
28
32
|
"@reach/auto-id": "0.17.0",
|
|
@@ -40,8 +44,6 @@
|
|
|
40
44
|
"autoprefixer": "10.4.7",
|
|
41
45
|
"axios": "0.24.0",
|
|
42
46
|
"bs-axios": "0.0.43",
|
|
43
|
-
"bs-css": "13.4.0",
|
|
44
|
-
"bs-css-emotion": "2.4.0",
|
|
45
47
|
"case": "1.6.3",
|
|
46
48
|
"click-outside-hook": "1.1.0",
|
|
47
49
|
"copy-to-clipboard": "3.3.1",
|
|
@@ -51,12 +53,19 @@
|
|
|
51
53
|
"downshift": "5.2.5",
|
|
52
54
|
"lenses-ppx": "6.1.10",
|
|
53
55
|
"list-selectors": "2.0.1",
|
|
56
|
+
"lodash": "4.17.21",
|
|
54
57
|
"postcss": "8.4.14",
|
|
55
58
|
"postcss-loader": "4.2.0",
|
|
56
59
|
"postcss-preset-env": "6.7.0",
|
|
57
60
|
"prismjs": "1.28.0",
|
|
61
|
+
"react": "18.2.0",
|
|
62
|
+
"react-big-calendar": "1.0.1",
|
|
58
63
|
"react-datepicker": "3.8.0",
|
|
64
|
+
"react-dom": "18.2.0",
|
|
65
|
+
"react-error-boundary": "3.1.4",
|
|
66
|
+
"react-helmet": "6.1.0",
|
|
59
67
|
"react-icons": "4.4.0",
|
|
68
|
+
"react-intl": "6.0.5",
|
|
60
69
|
"react-select": "5.4.0",
|
|
61
70
|
"react-table": "7.8.0",
|
|
62
71
|
"react-use": "17.4.0",
|
|
@@ -68,14 +77,7 @@
|
|
|
68
77
|
"rescript-react-update": "5.0.0",
|
|
69
78
|
"sanitize-html": "2.7.0",
|
|
70
79
|
"swr": "1.3.0",
|
|
71
|
-
"tailwindcss": "3.1.4"
|
|
72
|
-
"react-error-boundary": "3.1.4",
|
|
73
|
-
"@datadog/browser-rum": "4.14.0",
|
|
74
|
-
"react-intl": "6.0.5",
|
|
75
|
-
"lodash": "4.17.21",
|
|
76
|
-
"react-helmet": "6.1.0",
|
|
77
|
-
"react": "18.2.0",
|
|
78
|
-
"react-dom": "18.2.0"
|
|
80
|
+
"tailwindcss": "3.1.4"
|
|
79
81
|
},
|
|
80
82
|
"devDependencies": {
|
|
81
83
|
"@babel/core": "7.18.6",
|
package/src/Toolkit.res
CHANGED
|
@@ -12,3 +12,5 @@ module Form = Toolkit__Form
|
|
|
12
12
|
module FormValidationFunctions = Toolkit__FormValidationFunctions
|
|
13
13
|
module BrowserLogger = Toolkit__BrowserLogger
|
|
14
14
|
module NativeLogger = Toolkit__NativeLogger
|
|
15
|
+
module Intl = Toolkit__Intl
|
|
16
|
+
module LocalesHelpers = Toolkit__LocalesHelpers
|
|
@@ -166,12 +166,8 @@ module Make = (StateLenses: Config) => {
|
|
|
166
166
|
| "password" =>
|
|
167
167
|
<Toolkit__Ui_Tooltip
|
|
168
168
|
label={showPassword
|
|
169
|
-
? <ReactIntl.FormattedMessage
|
|
170
|
-
|
|
171
|
-
/>
|
|
172
|
-
: <ReactIntl.FormattedMessage
|
|
173
|
-
id="toolkit.showPassword" defaultMessage="Show password"
|
|
174
|
-
/>}>
|
|
169
|
+
? <ReactIntl.FormattedMessage defaultMessage="Masquer le mot de passe" />
|
|
170
|
+
: <ReactIntl.FormattedMessage defaultMessage="Afficher le mot de passe" />}>
|
|
175
171
|
<button
|
|
176
172
|
type_="button"
|
|
177
173
|
className="p-1 bg-neutral-300 rounded-r border border-gray-300 text-neutral-800"
|
|
@@ -2,20 +2,22 @@ open ReactIntl
|
|
|
2
2
|
|
|
3
3
|
module Msg = {
|
|
4
4
|
@@intl.messages
|
|
5
|
-
let requiredValue = {defaultMessage: "
|
|
6
|
-
let requiredPosInt = {defaultMessage: "
|
|
7
|
-
let requiredPosIntOrFloat = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
let
|
|
5
|
+
let requiredValue = {defaultMessage: "Champs requis"}
|
|
6
|
+
let requiredPosInt = {defaultMessage: "Doit etre un entier positif"}
|
|
7
|
+
let requiredPosIntOrFloat = {
|
|
8
|
+
defaultMessage: "Doit être un entier positif ou un nombre a virgule",
|
|
9
|
+
}
|
|
10
|
+
let wrongFormat = {defaultMessage: "Mauvais format (req: {exemple})"}
|
|
11
|
+
let maxNumberOfPackets = {defaultMessage: "Nbr colis"}
|
|
12
|
+
let totalVolume = {defaultMessage: "Volume total"}
|
|
11
13
|
let binPacking = {defaultMessage: "Bin packing"}
|
|
12
|
-
let vehicleNameAlreadyUsed = {defaultMessage: "
|
|
14
|
+
let vehicleNameAlreadyUsed = {defaultMessage: "Ce nom est deja utilisé dans un autre forfait"}
|
|
13
15
|
|
|
14
16
|
let requiredAlphanumericMax4 = {
|
|
15
|
-
defaultMessage: "
|
|
17
|
+
defaultMessage: "Format requis : 4 caractères ou nombres (exemples : 'A1b2' 'abcd')",
|
|
16
18
|
}
|
|
17
19
|
let required14Digits = {
|
|
18
|
-
defaultMessage: "
|
|
20
|
+
defaultMessage: "Format requis : 14 chiffres",
|
|
19
21
|
}
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
open ReactIntl
|
|
2
|
+
|
|
3
|
+
type rec messages = {fr: array<translation>}
|
|
4
|
+
and translation = {
|
|
5
|
+
id: string,
|
|
6
|
+
defaultMessage: string,
|
|
7
|
+
message: Js.nullable<string>,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let toDict = (translations: array<translation>) =>
|
|
11
|
+
translations->Array.reduce(Js.Dict.empty(), (dict, entry) => {
|
|
12
|
+
dict->Js.Dict.set(
|
|
13
|
+
entry.id,
|
|
14
|
+
entry.message->Js.Nullable.toOption->Option.getWithDefault(entry.defaultMessage),
|
|
15
|
+
)
|
|
16
|
+
dict
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
type availableLanguages = [
|
|
20
|
+
| #fr
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
let availableLanguagesToString = (availableLanguages: availableLanguages) =>
|
|
24
|
+
switch availableLanguages {
|
|
25
|
+
| #fr => "fr"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let availableLanguagesFromString = v =>
|
|
29
|
+
switch v {
|
|
30
|
+
| v if v->Js.String2.includes("fr") => #fr
|
|
31
|
+
| _ => #fr
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let createIntl = (locale: availableLanguages, messages) => {
|
|
35
|
+
let cache = createIntlCache()
|
|
36
|
+
|
|
37
|
+
createIntl(
|
|
38
|
+
intlConfig(
|
|
39
|
+
~locale=locale->availableLanguagesToString,
|
|
40
|
+
~messages,
|
|
41
|
+
~onError=message => Toolkit__BrowserLogger.error2("create intl error", message),
|
|
42
|
+
(),
|
|
43
|
+
),
|
|
44
|
+
cache,
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module type IntlConfig = {
|
|
49
|
+
let messages: messages
|
|
50
|
+
let defaultLocale: option<availableLanguages>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module Make = (Config: IntlConfig) => {
|
|
54
|
+
let browserLocale = Browser.Navigator.getBrowserLanguage()
|
|
55
|
+
let locale =
|
|
56
|
+
Config.defaultLocale->Option.getWithDefault(browserLocale->availableLanguagesFromString)
|
|
57
|
+
|
|
58
|
+
let messages = switch locale {
|
|
59
|
+
| #fr => Config.messages.fr->toDict
|
|
60
|
+
}
|
|
61
|
+
let intl = createIntl(locale, messages)
|
|
62
|
+
|
|
63
|
+
type state = {
|
|
64
|
+
locale: availableLanguages,
|
|
65
|
+
intl: Intl.t,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type action = SetLocale(availableLanguages)
|
|
69
|
+
|
|
70
|
+
let store = Restorative.createStore({locale: locale, intl: intl}, (_state, action) =>
|
|
71
|
+
switch action {
|
|
72
|
+
| SetLocale(locale) => {
|
|
73
|
+
locale: locale,
|
|
74
|
+
intl: {
|
|
75
|
+
let messages = switch locale {
|
|
76
|
+
| #fr => Config.messages.fr->toDict
|
|
77
|
+
}
|
|
78
|
+
createIntl(locale, messages)
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
let setCurrentLocale = (locale: availableLanguages) => store.dispatch(SetLocale(locale))
|
|
85
|
+
|
|
86
|
+
let useCurrentLocale = () => store.useStore().locale
|
|
87
|
+
let useIntl = () => store.useStore().intl
|
|
88
|
+
|
|
89
|
+
let getDateFnsLocale = locale =>
|
|
90
|
+
switch locale {
|
|
91
|
+
| #fr => BsDateFns.frLocale
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module Provider = {
|
|
95
|
+
@react.component
|
|
96
|
+
let make = (~children) => {
|
|
97
|
+
let intl = useIntl()
|
|
98
|
+
<RawIntlProvider value=intl> children </RawIntlProvider>
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
type availableLanguages = [
|
|
2
|
+
| #fr
|
|
3
|
+
]
|
|
4
|
+
type rec messages = {fr: array<translation>}
|
|
5
|
+
and translation = {
|
|
6
|
+
id: string,
|
|
7
|
+
defaultMessage: string,
|
|
8
|
+
message: Js.nullable<string>,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let availableLanguagesToString: availableLanguages => string
|
|
12
|
+
let availableLanguagesFromString: string => availableLanguages
|
|
13
|
+
|
|
14
|
+
module type IntlConfig = {
|
|
15
|
+
let messages: messages
|
|
16
|
+
let defaultLocale: option<availableLanguages>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module Make: (Config: IntlConfig) =>
|
|
20
|
+
{
|
|
21
|
+
let browserLocale: string
|
|
22
|
+
let locale: availableLanguages
|
|
23
|
+
let intl: ReactIntl.Intl.t
|
|
24
|
+
let useIntl: unit => ReactIntl.Intl.t
|
|
25
|
+
let useCurrentLocale: unit => availableLanguages
|
|
26
|
+
let setCurrentLocale: availableLanguages => unit
|
|
27
|
+
let getDateFnsLocale: availableLanguages => BsDateFns.dateFnsLocale
|
|
28
|
+
|
|
29
|
+
module Provider: {
|
|
30
|
+
@react.component
|
|
31
|
+
let make: (~children: React.element) => React.element
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
open ReactIntl
|
|
2
|
+
|
|
3
|
+
module DatePicker = {
|
|
4
|
+
module Fr = {
|
|
5
|
+
let weekdaysShort = ["Di", "Lun", "Ma", "Me", "Je", "Ve", "Sa"]
|
|
6
|
+
|
|
7
|
+
let months = [
|
|
8
|
+
j`Janvier`,
|
|
9
|
+
j`Février`,
|
|
10
|
+
j`Mars`,
|
|
11
|
+
j`Avril`,
|
|
12
|
+
j`Mai`,
|
|
13
|
+
j`Juin`,
|
|
14
|
+
j`Juillet`,
|
|
15
|
+
j`Août`,
|
|
16
|
+
j`Septembre`,
|
|
17
|
+
j`Octobre`,
|
|
18
|
+
j`Novembre`,
|
|
19
|
+
j`Décembre`,
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
let firstDayOfWeek = 1
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module Days = {
|
|
27
|
+
@@intl.messages
|
|
28
|
+
let monday = {defaultMessage: "Lundi"}
|
|
29
|
+
let tuesday = {defaultMessage: "Mardi"}
|
|
30
|
+
let wednesday = {defaultMessage: "Mercredi"}
|
|
31
|
+
let thursday = {defaultMessage: "Jeudi"}
|
|
32
|
+
let friday = {defaultMessage: "Vendredi"}
|
|
33
|
+
let saturday = {defaultMessage: "Samedi"}
|
|
34
|
+
let sunday = {defaultMessage: "Dimanche"}
|
|
35
|
+
}
|
package/src/intl/check.js
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
const fs = require(
|
|
2
|
-
const cp = require(
|
|
3
|
-
const path = require(
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const cp = require("child_process");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const cwd = process.cwd();
|
|
5
|
+
const translations = path.join(cwd, "locale");
|
|
4
6
|
|
|
5
|
-
const
|
|
6
|
-
const translations = path.join(cwd, 'locale', 'translations')
|
|
7
|
+
const AVAILABLE_LANGUAGES = ["fr"];
|
|
7
8
|
|
|
8
|
-
const
|
|
9
|
-
const file = path.join(translations, `${locale}.json`)
|
|
10
|
-
const content = JSON.parse(fs.readFileSync(file))
|
|
9
|
+
const missingMessagesLanguages = AVAILABLE_LANGUAGES.map((locale) => {
|
|
10
|
+
const file = path.join(translations, `${locale}.json`);
|
|
11
|
+
const content = JSON.parse(fs.readFileSync(file));
|
|
11
12
|
|
|
12
|
-
return content.filter((item) => item.message ===
|
|
13
|
-
})
|
|
13
|
+
return [content.filter((item) => item.message === "").length, locale];
|
|
14
|
+
});
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
16
|
+
missingMessagesLanguages.forEach(([missingMessagesLanguage, language]) => {
|
|
17
|
+
if (missingMessagesLanguage > 0) {
|
|
18
|
+
console.log(
|
|
19
|
+
`=== ⚠️ [${language}] There are ${missingMessagesLanguage} messages missing.`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
22
23
|
|
|
23
|
-
if (
|
|
24
|
-
process.exit(1)
|
|
24
|
+
if (missingMessagesLanguages.some(([missingMessages]) => missingMessages > 0)) {
|
|
25
|
+
process.exit(1);
|
|
25
26
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const glob = require("glob");
|
|
4
|
+
|
|
5
|
+
const cwd = process.cwd();
|
|
6
|
+
const originFile = path.join(cwd, "locale/translations/fr.json");
|
|
7
|
+
const base = JSON.parse(fs.readFileSync(originFile, "utf8"));
|
|
8
|
+
const dict = base.reduce((acc, value) => {
|
|
9
|
+
acc[value.defaultMessage] = value.message;
|
|
10
|
+
return acc;
|
|
11
|
+
}, {});
|
|
12
|
+
/**
|
|
13
|
+
* Match :
|
|
14
|
+
* - defaultMessage=""
|
|
15
|
+
* - defaultMessage={""}
|
|
16
|
+
* - defaultMessage:
|
|
17
|
+
*/
|
|
18
|
+
const pattern = /(defaultMessage(=|:|={|:\s))"(.*?|$)"/g;
|
|
19
|
+
|
|
20
|
+
glob("**/*.res", (err, files) => {
|
|
21
|
+
files.forEach((file) => {
|
|
22
|
+
const filePath = path.join(cwd, file);
|
|
23
|
+
let fileContent = fs.readFileSync(filePath, "utf-8");
|
|
24
|
+
const matches = fileContent.matchAll(pattern);
|
|
25
|
+
|
|
26
|
+
for (const match of matches) {
|
|
27
|
+
let [, , , key] = match;
|
|
28
|
+
let message = dict[key];
|
|
29
|
+
|
|
30
|
+
if (message) {
|
|
31
|
+
fileContent = fileContent.replace(`"${key}"`, `"${message}"`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fs.writeFileSync(filePath, fileContent);
|
|
36
|
+
});
|
|
37
|
+
});
|
package/src/intl/extract.js
CHANGED
|
@@ -1,33 +1,44 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const cp = require("child_process");
|
|
3
3
|
const path = require("path");
|
|
4
|
-
|
|
5
|
-
const locales = ["en", "fr"];
|
|
6
|
-
|
|
7
4
|
const cwd = process.cwd();
|
|
8
5
|
const src = path.join(cwd, "src");
|
|
9
|
-
const
|
|
10
|
-
|
|
6
|
+
const localeFolderPath = path.join(cwd, "locale");
|
|
7
|
+
|
|
8
|
+
const AVAILABLE_LANGUAGES = ["fr"];
|
|
9
|
+
const DEFAULT_LANGUAGE = "fr";
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(localeFolderPath)) {
|
|
12
|
+
fs.mkdirSync(localeFolderPath);
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
const bin = path.join(cwd, "node_modules", ".bin", "bs-react-intl-extractor");
|
|
12
16
|
|
|
13
17
|
console.log("=== ⏳ Extracting messages...");
|
|
14
18
|
const extracted = JSON.parse(
|
|
15
|
-
cp.execSync(`${bin} --allow-duplicates ${src} ${
|
|
19
|
+
cp.execSync(`${bin} --allow-duplicates ${src} ${localeFolderPath}`)
|
|
16
20
|
);
|
|
17
21
|
console.log("=== ✅ Extracting messages... done.");
|
|
18
22
|
|
|
19
|
-
for (const
|
|
20
|
-
console.log(`\n=== ⏳ Updating ${
|
|
21
|
-
const file = path.join(src, "../locale
|
|
23
|
+
for (const LANGUAGE of AVAILABLE_LANGUAGES) {
|
|
24
|
+
console.log(`\n=== ⏳ Updating ${LANGUAGE} translation...`);
|
|
25
|
+
const file = path.join(src, "../locale", `${LANGUAGE}.json`);
|
|
22
26
|
|
|
23
27
|
let content;
|
|
24
28
|
try {
|
|
25
|
-
|
|
29
|
+
if (fs.existsSync(file)) {
|
|
30
|
+
content = JSON.parse(fs.readFileSync(file));
|
|
31
|
+
} else {
|
|
32
|
+
console.log(
|
|
33
|
+
`=== ⚠️ Translation for ${LANGUAGE} wasn't found. Creating new one.`
|
|
34
|
+
);
|
|
35
|
+
content = [];
|
|
36
|
+
}
|
|
26
37
|
} catch (error) {
|
|
27
38
|
console.log(error.code);
|
|
28
39
|
if (error.code === "ENOENT") {
|
|
29
40
|
console.log(
|
|
30
|
-
`=== ⚠️ Translation for ${
|
|
41
|
+
`=== ⚠️ Translation for ${LANGUAGE} wasn't found. Creating new one.`
|
|
31
42
|
);
|
|
32
43
|
content = [];
|
|
33
44
|
} else {
|
|
@@ -42,11 +53,11 @@ for (const locale of locales) {
|
|
|
42
53
|
message:
|
|
43
54
|
cache[msg.id] && cache[msg.id].message
|
|
44
55
|
? cache[msg.id].message
|
|
45
|
-
:
|
|
56
|
+
: LANGUAGE === DEFAULT_LANGUAGE
|
|
46
57
|
? msg.defaultMessage
|
|
47
58
|
: "",
|
|
48
59
|
}));
|
|
49
60
|
|
|
50
61
|
fs.writeFileSync(file, JSON.stringify(messages, null, 2) + "\n");
|
|
51
|
-
console.log(`=== ✅ Updating ${
|
|
62
|
+
console.log(`=== ✅ Updating ${LANGUAGE} translation... done.`);
|
|
52
63
|
}
|
|
@@ -4,39 +4,9 @@ let make = (~children, ~className="", ~size: [#md | #xl]=#md) => {
|
|
|
4
4
|
<div
|
|
5
5
|
className={cx([
|
|
6
6
|
className,
|
|
7
|
-
"flex justify-center items-center rounded-full font-display bg-gray-200",
|
|
7
|
+
"flex justify-center items-center rounded-full font-display bg-gray-200 cw-ButtonGroup",
|
|
8
8
|
isMd ? "h-6" : "h-10",
|
|
9
9
|
isMd ? "text-sm" : "text-xl",
|
|
10
|
-
{
|
|
11
|
-
open Css
|
|
12
|
-
style(list{
|
|
13
|
-
selector(
|
|
14
|
-
"& > button",
|
|
15
|
-
list{
|
|
16
|
-
whiteSpace(#nowrap),
|
|
17
|
-
height(100.->pct),
|
|
18
|
-
padding2(~v=0.75->rem, ~h=isMd ? 0.75->rem : 1.->rem),
|
|
19
|
-
display(#flex),
|
|
20
|
-
alignItems(#center),
|
|
21
|
-
justifyContent(#center),
|
|
22
|
-
borderRadius(2.->rem),
|
|
23
|
-
selector("&:hover", list{backgroundColor("3DDEF3"->hex)}),
|
|
24
|
-
},
|
|
25
|
-
),
|
|
26
|
-
selector(
|
|
27
|
-
"& > button.selected",
|
|
28
|
-
list{
|
|
29
|
-
background(
|
|
30
|
-
linearGradient(
|
|
31
|
-
270.->deg,
|
|
32
|
-
list{(0.->pct, "11a3b6"->hex), (100.->pct, "27d0dc"->hex)},
|
|
33
|
-
),
|
|
34
|
-
),
|
|
35
|
-
color(white),
|
|
36
|
-
},
|
|
37
|
-
),
|
|
38
|
-
})
|
|
39
|
-
},
|
|
40
10
|
])}>
|
|
41
11
|
children
|
|
42
12
|
</div>
|
|
@@ -79,13 +79,7 @@ module Footer = {
|
|
|
79
79
|
@react.component
|
|
80
80
|
let make = (~className="", ~children=React.null, ~customMinHeight=200) =>
|
|
81
81
|
<div
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"rounded-lg shadow-sm bg-white p-6",
|
|
85
|
-
{
|
|
86
|
-
open Css
|
|
87
|
-
style(list{minHeight(customMinHeight->px)})
|
|
88
|
-
},
|
|
89
|
-
])}>
|
|
82
|
+
style={ReactDOMStyle.make(~minHeight=`${customMinHeight->Int.toString}px`, ())}
|
|
83
|
+
className={cx([className, "rounded-lg shadow-sm bg-white p-6"])}>
|
|
90
84
|
children
|
|
91
85
|
</div>
|
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
type size = [#xs | #sm | #md | #lg]
|
|
2
2
|
|
|
3
|
-
module Styles = {
|
|
4
|
-
open Css
|
|
5
|
-
|
|
6
|
-
let label = style(list{
|
|
7
|
-
selector("& > input + .checkmark", list{backgroundColor(hex("fff"))}),
|
|
8
|
-
selector(
|
|
9
|
-
"& > input:checked + .checkmark",
|
|
10
|
-
list{backgroundColor(hex("15cbe3")), borderColor(hex("15cbe3"))},
|
|
11
|
-
),
|
|
12
|
-
selector("& > input + .checkmark > *", list{opacity(0.)}),
|
|
13
|
-
selector("& > input:checked + .checkmark > *", list{opacity(1.)}),
|
|
14
|
-
})
|
|
15
|
-
}
|
|
16
|
-
|
|
17
3
|
@react.component
|
|
18
4
|
let make = (
|
|
19
5
|
~value,
|
|
@@ -45,8 +31,7 @@ let make = (
|
|
|
45
31
|
|
|
46
32
|
<label
|
|
47
33
|
className={cx([
|
|
48
|
-
|
|
49
|
-
"items-center",
|
|
34
|
+
"items-center cw-Checkbox",
|
|
50
35
|
inverseLabel ? "inline-flex flex-row-reverse justify-end" : "flex flex-row",
|
|
51
36
|
disabled->Option.getWithDefault(false)
|
|
52
37
|
? "cursor-not-allowed opacity-75 text-gray-600"
|
|
@@ -58,7 +43,7 @@ let make = (
|
|
|
58
43
|
type_="checkbox"
|
|
59
44
|
value
|
|
60
45
|
?defaultChecked
|
|
61
|
-
className="hidden"
|
|
46
|
+
className="hidden peer"
|
|
62
47
|
onChange={event => {
|
|
63
48
|
let target = ReactEvent.Form.target(event)
|
|
64
49
|
let checked = target["checked"]
|
|
@@ -73,7 +58,8 @@ let make = (
|
|
|
73
58
|
/>
|
|
74
59
|
<span
|
|
75
60
|
className={cx([
|
|
76
|
-
"
|
|
61
|
+
"peer-checked:bg-primary-500 peer-checked:border-primary-500",
|
|
62
|
+
"bg-white rounded border text-white border-neutral-300 transform transition-all ease-in-out flex items-center justify-center flex-shrink-0",
|
|
77
63
|
switch size {
|
|
78
64
|
| #xs => "w-4 h-4"
|
|
79
65
|
| #sm => "w-6 h-6"
|
|
@@ -2,19 +2,6 @@ type size = [#xs | #sm | #md | #lg | #custom(int)]
|
|
|
2
2
|
type color = [#primary | #success | #danger | #neutral]
|
|
3
3
|
type style = [#default | #colored(color)]
|
|
4
4
|
|
|
5
|
-
let sizeRule = size => {
|
|
6
|
-
let value = switch size {
|
|
7
|
-
| #xs => 480
|
|
8
|
-
| #sm => 600
|
|
9
|
-
| #md => 768
|
|
10
|
-
| #lg => 900
|
|
11
|
-
| #custom(value) => value
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
open Css
|
|
15
|
-
style(list{width(pct(100.)), important(maxWidth(px(value)))})
|
|
16
|
-
}
|
|
17
|
-
|
|
18
5
|
let modalStyle = (~type_) =>
|
|
19
6
|
switch type_ {
|
|
20
7
|
| #default => "rounded"
|
|
@@ -51,23 +38,6 @@ let closeIconStyle = (~type_) =>
|
|
|
51
38
|
| #colored(_) => "hover:bg-white hover:text-black text-white"
|
|
52
39
|
}
|
|
53
40
|
|
|
54
|
-
let blockBody = () => {
|
|
55
|
-
open Css
|
|
56
|
-
global("body", list{overflow(#hidden)})
|
|
57
|
-
}
|
|
58
|
-
let resetBody = () => {
|
|
59
|
-
open Css
|
|
60
|
-
global("body", list{overflow(#auto)})
|
|
61
|
-
}
|
|
62
|
-
{
|
|
63
|
-
open Css
|
|
64
|
-
global("[data-reach-dialog-overlay]", list{important(background(rgba(74, 115, 120, #num(0.5))))})
|
|
65
|
-
}
|
|
66
|
-
{
|
|
67
|
-
open Css
|
|
68
|
-
global("[data-reach-dialog-content]", list{important(background(transparent))})
|
|
69
|
-
}
|
|
70
|
-
|
|
71
41
|
@react.component
|
|
72
42
|
let make = (
|
|
73
43
|
~isVisible,
|
|
@@ -79,7 +49,28 @@ let make = (
|
|
|
79
49
|
~footer=?,
|
|
80
50
|
~ariaLabel="",
|
|
81
51
|
) =>
|
|
82
|
-
<ReachUi.Dialog
|
|
52
|
+
<ReachUi.Dialog
|
|
53
|
+
ariaLabel
|
|
54
|
+
isOpen=isVisible
|
|
55
|
+
onDismiss=hide
|
|
56
|
+
style={ReactDOMStyle.make(
|
|
57
|
+
~maxWidth={
|
|
58
|
+
let value = switch size {
|
|
59
|
+
| #xs => 480
|
|
60
|
+
| #sm => 600
|
|
61
|
+
| #md => 768
|
|
62
|
+
| #lg => 900
|
|
63
|
+
| #custom(value) => value
|
|
64
|
+
}
|
|
65
|
+
`${value->Int.toString}px`
|
|
66
|
+
},
|
|
67
|
+
(),
|
|
68
|
+
)}
|
|
69
|
+
className={cx([
|
|
70
|
+
{
|
|
71
|
+
"w-full "
|
|
72
|
+
},
|
|
73
|
+
])}>
|
|
83
74
|
<div className={cx(["bg-white pb-5 z-40 shadow-lg w-full", modalStyle(~type_)])}>
|
|
84
75
|
<header
|
|
85
76
|
className={cx([
|
|
@@ -4,6 +4,7 @@ type color = [#success | #danger | #warning | #info]
|
|
|
4
4
|
let make = (~progression: float, ~color: color=#success) =>
|
|
5
5
|
<div className="flex flex-row w-full h-2 bg-neutral-300 rounded-lg">
|
|
6
6
|
<div
|
|
7
|
+
style={ReactDOMStyle.make(~width=`${progression->Float.toString}%`, ())}
|
|
7
8
|
className={cx([
|
|
8
9
|
"h-full rounded-lg",
|
|
9
10
|
switch color {
|
|
@@ -12,7 +13,6 @@ let make = (~progression: float, ~color: color=#success) =>
|
|
|
12
13
|
| #warning => "bg-warning-500"
|
|
13
14
|
| #info => "bg-info-500"
|
|
14
15
|
},
|
|
15
|
-
Css.style(list{Css.width(progression->#percent)}),
|
|
16
16
|
])}
|
|
17
17
|
/>
|
|
18
18
|
</div>
|
|
@@ -1,20 +1,5 @@
|
|
|
1
1
|
type size = [#xs | #sm | #md | #lg]
|
|
2
2
|
|
|
3
|
-
module Styles = {
|
|
4
|
-
open Css
|
|
5
|
-
|
|
6
|
-
let label = style(list{
|
|
7
|
-
selector("& > input + .checkmark", list{backgroundColor(hex("fff"))}),
|
|
8
|
-
selector("& > input:focus + .checkmark", list{unsafe("boxShadow", "0px 0px 4px #15CBE3")}),
|
|
9
|
-
selector(
|
|
10
|
-
"& > input:checked + .checkmark",
|
|
11
|
-
list{backgroundColor(hex("15cbe3")), borderColor(hex("15cbe3"))},
|
|
12
|
-
),
|
|
13
|
-
selector("& > input + .checkmark > *", list{opacity(0.)}),
|
|
14
|
-
selector("& > input:checked + .checkmark > *", list{opacity(1.)}),
|
|
15
|
-
})
|
|
16
|
-
}
|
|
17
|
-
|
|
18
3
|
@react.component
|
|
19
4
|
let make = (
|
|
20
5
|
~value,
|
|
@@ -28,7 +13,6 @@ let make = (
|
|
|
28
13
|
) =>
|
|
29
14
|
<label
|
|
30
15
|
className={cx([
|
|
31
|
-
Styles.label,
|
|
32
16
|
"flex items-center",
|
|
33
17
|
disabled->Option.getWithDefault(false)
|
|
34
18
|
? "cursor-not-allowed opacity-75 text-gray-600"
|
|
@@ -38,7 +22,7 @@ let make = (
|
|
|
38
22
|
<input
|
|
39
23
|
type_="radio"
|
|
40
24
|
value
|
|
41
|
-
className="opacity-0 w-0 h-0"
|
|
25
|
+
className="opacity-0 w-0 h-0 peer"
|
|
42
26
|
onChange={event => {
|
|
43
27
|
let target = ReactEvent.Form.target(event)
|
|
44
28
|
let value = target["value"]
|
|
@@ -51,6 +35,8 @@ let make = (
|
|
|
51
35
|
/>
|
|
52
36
|
<span
|
|
53
37
|
className={cx([
|
|
38
|
+
"bg-white peer-focus:shadow-sm peer-focus:shadow-primary-500",
|
|
39
|
+
"peer-checked:border-primary-500 peer-checked:bg-primary-500",
|
|
54
40
|
"checkmark rounded-full border text-white mr-3 border-neutral-300 transform transition-all ease-in-out flex items-center justify-center",
|
|
55
41
|
switch size {
|
|
56
42
|
| #xs => "w-4 h-4"
|
|
@@ -38,32 +38,23 @@ let make = (
|
|
|
38
38
|
{isVisible && canBeShowed
|
|
39
39
|
? <ReachUi.Portal>
|
|
40
40
|
<div
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
top(
|
|
59
|
-
triggerRect
|
|
60
|
-
->Js.Nullable.toOption
|
|
61
|
-
->Option.mapWithDefault(0, triggerRect => triggerRect.bottom + scrollY)
|
|
62
|
-
->px,
|
|
63
|
-
),
|
|
64
|
-
})
|
|
65
|
-
},
|
|
66
|
-
])}
|
|
41
|
+
style={ReactDOMStyle.make(
|
|
42
|
+
~borderLeft="10px solid transparent",
|
|
43
|
+
~borderRight="10px solid transparent",
|
|
44
|
+
~borderBottom="10px solid",
|
|
45
|
+
~left=`${triggerRect
|
|
46
|
+
->Js.Nullable.toOption
|
|
47
|
+
->Option.mapWithDefault(0, triggerRect =>
|
|
48
|
+
triggerRect.left - 10 + triggerRect.width / 2
|
|
49
|
+
)
|
|
50
|
+
->Int.toString}px`,
|
|
51
|
+
~top=`${triggerRect
|
|
52
|
+
->Js.Nullable.toOption
|
|
53
|
+
->Option.mapWithDefault(0, triggerRect => triggerRect.bottom + scrollY)
|
|
54
|
+
->Int.toString}px`,
|
|
55
|
+
(),
|
|
56
|
+
)}
|
|
57
|
+
className={cx(["absolute w-0 h-0 border-gray-800", triangleClassName])}
|
|
67
58
|
/>
|
|
68
59
|
</ReachUi.Portal>
|
|
69
60
|
: React.null}
|
|
@@ -71,10 +62,10 @@ let make = (
|
|
|
71
62
|
? <Toolkit__Ui_Spread props=tooltip>
|
|
72
63
|
<TooltipPopup
|
|
73
64
|
label
|
|
65
|
+
style={ReactDOMStyle.make(~maxWidth=`${maxWidth->Int.toString}px`, ())}
|
|
74
66
|
className={cx([
|
|
75
67
|
"px-3 py-2 bg-gray-800 rounded text-white text-sm z-40 whitespace-pre-wrap",
|
|
76
68
|
tooltipClassName,
|
|
77
|
-
Css.style(list{Css.maxWidth(maxWidth->Css.px)}),
|
|
78
69
|
])}
|
|
79
70
|
position=centered
|
|
80
71
|
/>
|
package/src/ui/styles.css
CHANGED
|
@@ -39,12 +39,13 @@ body,
|
|
|
39
39
|
|
|
40
40
|
[data-reach-dialog-content] {
|
|
41
41
|
padding: 0 !important;
|
|
42
|
+
background: transparent;
|
|
42
43
|
margin: 10vh auto;
|
|
43
44
|
outline: 0;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
[data-reach-dialog-overlay] {
|
|
47
|
-
background: rgba(
|
|
48
|
+
background: rgba(74, 115, 120, 0.5);
|
|
48
49
|
z-index: 40;
|
|
49
50
|
position: fixed;
|
|
50
51
|
top: 0;
|
|
@@ -376,4 +377,20 @@ input[type=number] {
|
|
|
376
377
|
color: #15373a;
|
|
377
378
|
}
|
|
378
379
|
|
|
380
|
+
.cw-ButtonGroup > button {
|
|
381
|
+
@apply whitespace-nowrap h-full px-3 md:px-4 flex items-center justify-center rounded-3xl py-3
|
|
382
|
+
}
|
|
383
|
+
.cw-ButtonGroup > button:hover {
|
|
384
|
+
@apply bg-primary-300
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.cw-ButtonGroup > button.selected {
|
|
388
|
+
background: linear-gradient(270deg, #11a3b6 0%, #27d0dc 100%);
|
|
389
|
+
color: white;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.cw-Checkbox > input:checked + .checkmark > * {
|
|
393
|
+
@apply opacity-100
|
|
394
|
+
}
|
|
395
|
+
|
|
379
396
|
/* purgecss end ignore */
|
package/src/vendors/Browser.res
CHANGED
|
@@ -56,3 +56,19 @@ external screenWidth: int = "window.screen.width"
|
|
|
56
56
|
|
|
57
57
|
@val
|
|
58
58
|
external screenHeight: int = "window.screen.height"
|
|
59
|
+
|
|
60
|
+
module Navigator = {
|
|
61
|
+
type t
|
|
62
|
+
|
|
63
|
+
@val
|
|
64
|
+
external userLanguage: option<string> = "window.navigator.userLanguage"
|
|
65
|
+
|
|
66
|
+
@val external language: option<string> = "window.navigator.language"
|
|
67
|
+
|
|
68
|
+
let getBrowserLanguage = () =>
|
|
69
|
+
switch (userLanguage, language) {
|
|
70
|
+
| (Some(l), _) => l
|
|
71
|
+
| (_, Some(l)) => l
|
|
72
|
+
| (None, None) => "fr"
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/vendors/Browser.resi
CHANGED
|
@@ -36,3 +36,14 @@ module DomElement: {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
let innerWidth: int
|
|
39
|
+
|
|
40
|
+
module Navigator: {
|
|
41
|
+
type t
|
|
42
|
+
|
|
43
|
+
@val
|
|
44
|
+
external userLanguage: option<string> = "window.navigator.userLanguage"
|
|
45
|
+
|
|
46
|
+
@val external language: option<string> = "window.navigator.language"
|
|
47
|
+
|
|
48
|
+
let getBrowserLanguage: unit => string
|
|
49
|
+
}
|