@scrabble-solver/scrabble-solver 2.9.3 → 2.10.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.
Files changed (118) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +11 -11
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/cache/eslint/.cache_8dgz12 +1 -1
  5. package/.next/cache/next-server.js.nft.json +1 -1
  6. package/.next/cache/webpack/client-production/0.pack +0 -0
  7. package/.next/cache/webpack/client-production/index.pack +0 -0
  8. package/.next/cache/webpack/edge-server-production/0.pack +0 -0
  9. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  10. package/.next/cache/webpack/server-production/0.pack +0 -0
  11. package/.next/cache/webpack/server-production/index.pack +0 -0
  12. package/.next/next-server.js.nft.json +1 -1
  13. package/.next/prerender-manifest.json +1 -1
  14. package/.next/routes-manifest.json +1 -1
  15. package/.next/server/chunks/{429.js → 131.js} +2 -3652
  16. package/.next/server/chunks/413.js +367 -185
  17. package/.next/server/chunks/44.js +71 -45
  18. package/.next/server/chunks/515.js +1026 -480
  19. package/.next/server/chunks/911.js +96 -2
  20. package/.next/server/middleware-build-manifest.js +1 -1
  21. package/.next/server/pages/404.html +2 -2
  22. package/.next/server/pages/404.js.nft.json +1 -1
  23. package/.next/server/pages/500.html +2 -2
  24. package/.next/server/pages/_app.js.nft.json +1 -1
  25. package/.next/server/pages/_error.js.nft.json +1 -1
  26. package/.next/server/pages/api/dictionary/[locale]/[word].js +48 -10
  27. package/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
  28. package/.next/server/pages/api/dictionary/[locale].js +1 -8
  29. package/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -1
  30. package/.next/server/pages/api/solve.js +62 -49
  31. package/.next/server/pages/api/solve.js.nft.json +1 -1
  32. package/.next/server/pages/api/verify.js +2 -9
  33. package/.next/server/pages/api/verify.js.nft.json +1 -1
  34. package/.next/server/pages/index.html +9 -1
  35. package/.next/server/pages/index.js +43 -36
  36. package/.next/server/pages/index.js.nft.json +1 -1
  37. package/.next/server/pages/index.json +1 -1
  38. package/.next/server/pages-manifest.json +2 -2
  39. package/.next/static/chunks/368-d423e70be6c0c473.js +1 -0
  40. package/.next/static/chunks/pages/{404-8650986dc56dab75.js → 404-932294135c3206dd.js} +1 -1
  41. package/.next/static/chunks/pages/_app-3f5508a5f544d9eb.js +1 -0
  42. package/.next/static/chunks/pages/index-8af7a9d7a2cd98a7.js +1 -0
  43. package/.next/static/css/6b1833fd19d3a74a.css +1 -0
  44. package/.next/static/css/a6154e4ca046ca13.css +1 -0
  45. package/.next/static/css/bad53af6f8616677.css +1 -0
  46. package/.next/static/vscqn7BEtAxJteWSwNnas/_buildManifest.js +1 -0
  47. package/.next/static/{s5pE_3C3ebZuSfaqE7xat → vscqn7BEtAxJteWSwNnas}/_ssgManifest.js +0 -0
  48. package/.next/trace +52 -52
  49. package/package.json +9 -9
  50. package/src/components/Board/components/Cell/Cell.module.scss +45 -9
  51. package/src/components/Board/hooks/useGrid.ts +4 -2
  52. package/src/components/Dictionary/Dictionary.module.scss +8 -1
  53. package/src/components/EmptyState/EmptyState.tsx +6 -2
  54. package/src/components/Loading/Loading.tsx +13 -2
  55. package/src/components/NavButtons/NavButtons.module.scss +8 -7
  56. package/src/components/NavButtons/NavButtons.tsx +35 -28
  57. package/src/components/PlainTiles/PlainTiles.tsx +7 -1
  58. package/src/components/PlainTiles/Tile.tsx +4 -3
  59. package/src/components/Rack/Rack.tsx +4 -2
  60. package/src/components/Radio/Radio.module.scss +1 -1
  61. package/src/components/RemainingTiles/Character.tsx +7 -2
  62. package/src/components/RemainingTiles/RemainingTiles.tsx +28 -20
  63. package/src/components/Results/Cell.tsx +5 -3
  64. package/src/components/Results/Result.tsx +8 -3
  65. package/src/components/Results/Results.module.scss +31 -9
  66. package/src/components/Results/Results.tsx +6 -2
  67. package/src/components/Results/getColumns.ts +58 -0
  68. package/src/components/Settings/components/ConfigSetting/ConfigSetting.module.scss +1 -0
  69. package/src/components/Settings/components/LocaleSetting/LocaleSetting.module.scss +12 -1
  70. package/src/components/Settings/components/LocaleSetting/LocaleSetting.tsx +1 -1
  71. package/src/components/Settings/components/LocaleSetting/options.ts +7 -1
  72. package/src/components/Sidebar/Sidebar.module.scss +42 -8
  73. package/src/components/SvgFontCss/SvgFontCss.tsx +8 -7
  74. package/src/components/SvgFontCss/createCss.ts +11 -0
  75. package/src/components/SvgFontCss/createStyle.ts +9 -0
  76. package/src/components/SvgFontCss/createSvg.ts +10 -0
  77. package/src/components/SvgFontFix/SvgFontFix.module.scss +5 -0
  78. package/src/components/SvgFontFix/SvgFontFix.tsx +21 -0
  79. package/src/components/SvgFontFix/index.ts +1 -0
  80. package/src/components/Tile/Tile.module.scss +10 -2
  81. package/src/components/Tile/Tile.tsx +4 -0
  82. package/src/components/Tile/TilePure.tsx +3 -1
  83. package/src/components/Words/Words.tsx +4 -3
  84. package/src/components/index.ts +1 -0
  85. package/src/hooks/index.ts +2 -0
  86. package/src/hooks/useDirection.ts +22 -0
  87. package/src/hooks/useLanguage.ts +22 -0
  88. package/src/i18n/constants.ts +53 -0
  89. package/src/i18n/fa.json +56 -0
  90. package/src/i18n/i18n.ts +22 -0
  91. package/src/i18n/index.ts +2 -20
  92. package/src/icons/FlagFa.svg +95 -0
  93. package/src/icons/index.ts +1 -0
  94. package/src/lib/createComparator.ts +22 -0
  95. package/src/lib/createKeyComparator.ts +4 -2
  96. package/src/lib/createStringComparator.ts +5 -0
  97. package/src/lib/detectLocale.ts +4 -0
  98. package/src/lib/getRemainingTiles.ts +4 -2
  99. package/src/lib/getRemainingTilesGroups.ts +48 -23
  100. package/src/lib/index.ts +2 -2
  101. package/src/lib/sortResults.ts +11 -9
  102. package/src/lib/unorderedArraysEqual.ts +3 -2
  103. package/src/pages/api/verify.ts +1 -1
  104. package/src/pages/index.module.scss +7 -18
  105. package/src/pages/index.tsx +10 -2
  106. package/src/service-worker/routeVerifyRequests.ts +1 -1
  107. package/src/state/selectors.ts +5 -5
  108. package/src/styles/global.scss +6 -1
  109. package/.next/static/chunks/880-18e3aea6915aebe7.js +0 -1
  110. package/.next/static/chunks/pages/_app-183f598b1d4d480b.js +0 -1
  111. package/.next/static/chunks/pages/index-f1cef1e67e14d022.js +0 -1
  112. package/.next/static/css/751e8a14776d05d8.css +0 -1
  113. package/.next/static/css/ad2a08918868cad8.css +0 -1
  114. package/.next/static/css/e8de67ad5ea35427.css +0 -1
  115. package/.next/static/s5pE_3C3ebZuSfaqE7xat/_buildManifest.js +0 -1
  116. package/src/components/Results/constants.ts +0 -42
  117. package/src/lib/comparator.ts +0 -16
  118. package/src/lib/stringComparator.ts +0 -5
@@ -54,6 +54,10 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
54
54
 
55
55
  flex: 0 1 auto;
56
56
  text-align: left;
57
+
58
+ [dir='rtl'] & {
59
+ text-align: right;
60
+ }
57
61
  }
58
62
 
59
63
  .result {
@@ -78,6 +82,10 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
78
82
  align-items: center;
79
83
  justify-content: space-between;
80
84
 
85
+ [dir='rtl'] & {
86
+ flex-direction: row-reverse;
87
+ }
88
+
81
89
  .word {
82
90
  @include ellipsis;
83
91
  }
@@ -88,17 +96,32 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
88
96
  align-items: center;
89
97
  justify-content: center;
90
98
  padding: var(--spacing--s);
99
+ gap: var(--spacing--s);
91
100
 
92
101
  .result &:first-child,
93
102
  .headerButton:first-child & {
94
- padding-left: $row-padding-horizontal;
95
103
  justify-content: flex-start;
96
- text-align: left;
104
+
105
+ [dir='ltr'] & {
106
+ padding-left: $row-padding-horizontal;
107
+ text-align: left;
108
+ }
109
+
110
+ [dir='rtl'] & {
111
+ padding-right: $row-padding-horizontal;
112
+ text-align: right;
113
+ }
97
114
  }
98
115
 
99
116
  .result &:last-child,
100
117
  .headerButton:last-child & {
101
- padding-right: $row-padding-horizontal;
118
+ [dir='ltr'] & {
119
+ padding-right: $row-padding-horizontal;
120
+ }
121
+
122
+ [dir='rtl'] & {
123
+ padding-left: $row-padding-horizontal;
124
+ }
102
125
  }
103
126
 
104
127
  &:last-child {
@@ -107,10 +130,7 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
107
130
  }
108
131
 
109
132
  .word {
110
- $width: 180px;
111
-
112
- flex: 0 0 $width;
113
- max-width: $width;
133
+ flex: 1 0 180px;
114
134
  text-transform: uppercase;
115
135
  }
116
136
 
@@ -123,7 +143,10 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
123
143
  }
124
144
 
125
145
  .points {
126
- flex: 1 0 80px;
146
+ $width: 80px;
147
+
148
+ flex: 1 0 $width;
149
+ max-width: $width;
127
150
  font-weight: bold;
128
151
  }
129
152
 
@@ -143,7 +166,6 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
143
166
  flex: 0 0 auto;
144
167
  width: $size;
145
168
  height: $size;
146
- margin-left: var(--spacing--s);
147
169
  }
148
170
 
149
171
  .input {
@@ -2,10 +2,12 @@ import classNames from 'classnames';
2
2
  import { FunctionComponent, useLayoutEffect, useRef } from 'react';
3
3
  import { FixedSizeList } from 'react-window';
4
4
 
5
+ import { LOCALE_FEATURES } from 'i18n';
5
6
  import { RESULTS_HEADER_HEIGHT, RESULTS_INPUT_HEIGHT, RESULTS_ITEM_HEIGHT } from 'parameters';
6
7
  import {
7
8
  selectAreResultsOutdated,
8
9
  selectIsLoading,
10
+ selectLocale,
9
11
  selectSolveError,
10
12
  selectSortedFilteredResults,
11
13
  selectSortedResults,
@@ -17,7 +19,7 @@ import EmptyState from '../EmptyState';
17
19
  import Loading from '../Loading';
18
20
  import ResultsInput from '../ResultsInput';
19
21
 
20
- import { COLUMNS } from './constants';
22
+ import getColumns from './getColumns';
21
23
  import HeaderButton from './HeaderButton';
22
24
  import Result from './Result';
23
25
  import styles from './Results.module.scss';
@@ -29,6 +31,7 @@ interface Props {
29
31
  }
30
32
 
31
33
  const Results: FunctionComponent<Props> = ({ height, width }) => {
34
+ const locale = useTypedSelector(selectLocale);
32
35
  const translate = useTranslate();
33
36
  const allResults = useTypedSelector(selectSortedResults);
34
37
  const results = useTypedSelector(selectSortedFilteredResults);
@@ -36,6 +39,7 @@ const Results: FunctionComponent<Props> = ({ height, width }) => {
36
39
  const isOutdated = useTypedSelector(selectAreResultsOutdated);
37
40
  const error = useTypedSelector(selectSolveError);
38
41
  const listRef = useRef<HTMLElement>();
42
+ const columns = getColumns(LOCALE_FEATURES[locale]);
39
43
 
40
44
  useLayoutEffect(() => {
41
45
  if (listRef.current) {
@@ -46,7 +50,7 @@ const Results: FunctionComponent<Props> = ({ height, width }) => {
46
50
  return (
47
51
  <div className={styles.results}>
48
52
  <div className={styles.header}>
49
- {COLUMNS.map((column) => (
53
+ {columns.map((column) => (
50
54
  <HeaderButton column={column} key={column.id} />
51
55
  ))}
52
56
  </div>
@@ -0,0 +1,58 @@
1
+ import { ResultColumn } from 'types';
2
+
3
+ import styles from './Results.module.scss';
4
+ import { Column } from './types';
5
+
6
+ const getColumns = (options: { consonants: boolean; vowels: boolean }): Column[] => {
7
+ const { consonants, vowels } = options;
8
+ const columns: Column[] = [
9
+ {
10
+ className: styles.word,
11
+ id: ResultColumn.Word,
12
+ translationKey: 'common.word',
13
+ },
14
+ {
15
+ className: styles.stat,
16
+ id: ResultColumn.TilesCount,
17
+ translationKey: 'common.tiles',
18
+ },
19
+ ];
20
+
21
+ if (consonants) {
22
+ columns.push({
23
+ className: styles.stat,
24
+ id: ResultColumn.ConsonantsCount,
25
+ translationKey: 'common.consonants',
26
+ });
27
+ }
28
+
29
+ if (vowels) {
30
+ columns.push({
31
+ className: styles.stat,
32
+ id: ResultColumn.VowelsCount,
33
+ translationKey: 'common.vowels',
34
+ });
35
+ }
36
+
37
+ columns.push(
38
+ {
39
+ className: styles.stat,
40
+ id: ResultColumn.BlanksCount,
41
+ translationKey: 'common.blanks',
42
+ },
43
+ {
44
+ className: styles.stat,
45
+ id: ResultColumn.WordsCount,
46
+ translationKey: 'common.words',
47
+ },
48
+ {
49
+ className: styles.points,
50
+ id: ResultColumn.Points,
51
+ translationKey: 'common.points',
52
+ },
53
+ );
54
+
55
+ return columns;
56
+ };
57
+
58
+ export default getColumns;
@@ -9,4 +9,5 @@
9
9
  .label {
10
10
  display: flex;
11
11
  align-items: center;
12
+ font-family: 'Open Sans', sans-serif;
12
13
  }
@@ -18,13 +18,18 @@
18
18
  .label {
19
19
  display: flex;
20
20
  align-items: center;
21
+ gap: var(--spacing--l);
22
+ font-family: 'Open Sans', sans-serif;
23
+
24
+ &.fa {
25
+ font-family: 'Vazirmatn', 'Open Sans', sans-serif;
26
+ }
21
27
  }
22
28
 
23
29
  .flag {
24
30
  $height: 32px;
25
31
 
26
32
  height: $height;
27
- margin-right: var(--spacing--l);
28
33
  box-shadow: var(--box-shadow--null);
29
34
  transition: var(--transition);
30
35
 
@@ -40,6 +45,12 @@
40
45
  width: $height * $aspect-ratio;
41
46
  }
42
47
 
48
+ &.fa {
49
+ $aspect-ratio: 1.75;
50
+
51
+ width: $height * $aspect-ratio;
52
+ }
53
+
43
54
  &.gb {
44
55
  $aspect-ratio: 2;
45
56
 
@@ -39,7 +39,7 @@ const LocaleSetting: FunctionComponent<Props> = ({ className, disabled }) => {
39
39
  value={option.value}
40
40
  onChange={handleChange}
41
41
  >
42
- <span className={styles.label}>
42
+ <span className={classNames(styles.label, option.className)}>
43
43
  <Icon className={classNames(styles.flag, option.className)} />
44
44
 
45
45
  <span>{option.label}</span>
@@ -1,7 +1,7 @@
1
1
  import { Locale } from '@scrabble-solver/types';
2
2
  import { FunctionComponent, SVGAttributes } from 'react';
3
3
 
4
- import { FlagEs, FlagFr, FlagGb, FlagPl, FlagUs, FlagDe } from 'icons';
4
+ import { FlagEs, FlagFa, FlagFr, FlagGb, FlagPl, FlagUs, FlagDe } from 'icons';
5
5
 
6
6
  import styles from './LocaleSetting.module.scss';
7
7
 
@@ -25,6 +25,12 @@ const options: Option[] = [
25
25
  label: 'English (US)',
26
26
  value: Locale.EN_US,
27
27
  },
28
+ {
29
+ className: styles.fa,
30
+ Icon: FlagFa,
31
+ label: 'فارسی',
32
+ value: Locale.FA_IR,
33
+ },
28
34
  {
29
35
  className: styles.fr,
30
36
  Icon: FlagFr,
@@ -3,10 +3,21 @@
3
3
  $z-index: 100;
4
4
  $width: 370px;
5
5
 
6
+ .modal,
7
+ .closeButton {
8
+ top: 0;
9
+
10
+ [dir='ltr'] & {
11
+ right: 0;
12
+ }
13
+
14
+ [dir='rtl'] & {
15
+ left: 0;
16
+ }
17
+ }
18
+
6
19
  .modal {
7
20
  position: fixed;
8
- top: 0;
9
- right: 0;
10
21
  z-index: $z-index;
11
22
  height: 100vh;
12
23
  width: $width;
@@ -21,15 +32,25 @@ $width: 370px;
21
32
 
22
33
  .afterOpen {
23
34
  .sidebar {
24
- transform: translateX(0);
25
- opacity: 1;
35
+ [dir='ltr'] &,
36
+ [dir='rtl'] & {
37
+ transform: translateX(0);
38
+ opacity: 1;
39
+ }
26
40
  }
27
41
  }
28
42
 
29
43
  .beforeClose {
30
44
  .sidebar {
31
- transform: translateX($width);
32
45
  opacity: 0;
46
+
47
+ [dir='ltr'] & {
48
+ transform: translateX($width);
49
+ }
50
+
51
+ [dir='rtl'] & {
52
+ transform: translateX(-$width);
53
+ }
33
54
  }
34
55
  }
35
56
 
@@ -47,6 +68,14 @@ $width: 370px;
47
68
  transition-duration: var(--transition--duration--long);
48
69
  transform: translateX($width);
49
70
  opacity: 0;
71
+
72
+ [dir='ltr'] & {
73
+ transform: translateX($width);
74
+ }
75
+
76
+ [dir='rtl'] & {
77
+ transform: translateX(-$width);
78
+ }
50
79
  }
51
80
 
52
81
  .header {
@@ -56,15 +85,20 @@ $width: 370px;
56
85
  }
57
86
 
58
87
  .title {
59
- padding-right: calc(var(--square-button-size) + var(--spacing--s));
60
88
  font-size: var(--font--size--h1);
61
89
  font-weight: 300;
90
+
91
+ [dir='ltr'] & {
92
+ padding-right: calc(var(--square-button-size) + var(--spacing--s));
93
+ }
94
+
95
+ [dir='rtl'] & {
96
+ padding-left: calc(var(--square-button-size) + var(--spacing--s));
97
+ }
62
98
  }
63
99
 
64
100
  .closeButton {
65
101
  position: absolute !important;
66
- top: 0;
67
- right: 0;
68
102
  z-index: $z-index + 1;
69
103
  margin: var(--spacing--l);
70
104
  }
@@ -1,13 +1,14 @@
1
1
  import { FunctionComponent } from 'react';
2
2
 
3
- // eslint-disable-next-line max-len
4
- const CSS = `@import url('https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&family=Open+Sans:wght@400;700&family=Roboto+Mono&display=swap');
3
+ import createCss from './createCss';
5
4
 
6
- text {
7
- font-family: 'Open Sans';
8
- }`;
5
+ interface Props {
6
+ fontFamily: string;
7
+ }
9
8
 
10
- // eslint-disable-next-line react/no-danger
11
- const SvgFontCss: FunctionComponent = () => <style type="text/css" dangerouslySetInnerHTML={{ __html: CSS }} />;
9
+ const SvgFontCss: FunctionComponent<Props> = ({ fontFamily }) => {
10
+ // eslint-disable-next-line react/no-danger
11
+ return <style type="text/css" dangerouslySetInnerHTML={{ __html: createCss(fontFamily) }} />;
12
+ };
12
13
 
13
14
  export default SvgFontCss;
@@ -0,0 +1,11 @@
1
+ const importUrl =
2
+ // eslint-disable-next-line max-len
3
+ 'https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&family=Open+Sans:wght@400;700&family=Vazirmatn:wght@300;400;700&family=Roboto+Mono&display=swap';
4
+
5
+ const createCss = (fontFamily: string): string => `@import url('${importUrl}');
6
+
7
+ text {
8
+ font-family: '${fontFamily}';
9
+ }`;
10
+
11
+ export default createCss;
@@ -0,0 +1,9 @@
1
+ import createCss from './createCss';
2
+
3
+ const createStyle = (fontFamily: string) => {
4
+ const style = document.createElement('style');
5
+ style.innerHTML = createCss(fontFamily);
6
+ return style;
7
+ };
8
+
9
+ export default createStyle;
@@ -0,0 +1,10 @@
1
+ import createStyle from './createStyle';
2
+
3
+ const createSvg = (fontFamily: string) => {
4
+ const svg = document.createElement('svg');
5
+ svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
6
+ svg.appendChild(createStyle(fontFamily));
7
+ return svg;
8
+ };
9
+
10
+ export default createSvg;
@@ -0,0 +1,5 @@
1
+ .svgFontFix {
2
+ width: 0;
3
+ height: 0;
4
+ position: absolute;
5
+ }
@@ -0,0 +1,21 @@
1
+ import { FunctionComponent } from 'react';
2
+
3
+ import { LOCALE_FEATURES } from 'i18n';
4
+
5
+ import SvgFontCss from '../SvgFontCss';
6
+
7
+ import styles from './SvgFontFix.module.scss';
8
+
9
+ const SvgFontFix: FunctionComponent = () => {
10
+ const fontFamilies = Array.from(new Set(Object.values(LOCALE_FEATURES).map(({ fontFamily }) => fontFamily)));
11
+
12
+ return (
13
+ <svg className={styles.svgFontFix} xmlns="http://www.w3.org/2000/svg">
14
+ {fontFamilies.map((fontFamily) => (
15
+ <SvgFontCss fontFamily={fontFamily} key={fontFamily} />
16
+ ))}
17
+ </svg>
18
+ );
19
+ };
20
+
21
+ export default SvgFontFix;
@@ -0,0 +1 @@
1
+ export { default } from './SvgFontFix';
@@ -73,10 +73,18 @@
73
73
 
74
74
  .points {
75
75
  position: absolute;
76
- right: 9%;
77
- bottom: 1%;
78
76
  font-weight: bold;
79
77
  user-select: none;
80
78
  pointer-events: none;
81
79
  letter-spacing: -1px;
80
+
81
+ [dir='ltr'] & {
82
+ bottom: 1%;
83
+ right: 9%;
84
+ }
85
+
86
+ [dir='rtl'] & {
87
+ top: 1%;
88
+ left: 9%;
89
+ }
82
90
  }
@@ -11,6 +11,7 @@ import {
11
11
  } from 'react';
12
12
 
13
13
  import { getTileSizes, noop } from 'lib';
14
+ import { selectLocale, useTypedSelector } from 'state';
14
15
 
15
16
  import TilePure from './TilePure';
16
17
 
@@ -49,6 +50,7 @@ const Tile: FunctionComponent<Props> = ({
49
50
  onFocus = noop,
50
51
  onKeyDown = noop,
51
52
  }) => {
53
+ const locale = useTypedSelector(selectLocale);
52
54
  const { pointsFontSize, tileFontSize, tileSize } = getTileSizes(size);
53
55
  const style = useMemo(() => ({ height: tileSize, width: tileSize }), [tileSize]);
54
56
  const inputStyle = useMemo(() => ({ fontSize: tileFontSize }), [tileFontSize]);
@@ -56,6 +58,7 @@ const Tile: FunctionComponent<Props> = ({
56
58
  const inputRef = useMemo<RefObject<HTMLInputElement>>(() => ref || createRef(), [ref]);
57
59
  const isEmpty = !character || character === EMPTY_CELL;
58
60
  const canShowPoints = (isBlank || !isEmpty) && typeof points !== 'undefined';
61
+ const pointsFormatted = typeof points === 'number' ? points.toLocaleString(locale) : '';
59
62
 
60
63
  const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (event) => {
61
64
  inputRef.current?.select();
@@ -81,6 +84,7 @@ const Tile: FunctionComponent<Props> = ({
81
84
  isBlank={isBlank}
82
85
  placeholder={placeholder}
83
86
  points={points}
87
+ pointsFormatted={pointsFormatted}
84
88
  pointsStyle={pointsStyle}
85
89
  raised={raised}
86
90
  style={style}
@@ -23,6 +23,7 @@ interface Props {
23
23
  isBlank?: boolean;
24
24
  placeholder?: string;
25
25
  points?: number;
26
+ pointsFormatted?: string;
26
27
  pointsStyle?: CSSProperties;
27
28
  raised?: boolean;
28
29
  style?: CSSProperties;
@@ -44,6 +45,7 @@ const TilePure: FunctionComponent<Props> = ({
44
45
  isBlank,
45
46
  placeholder,
46
47
  points,
48
+ pointsFormatted,
47
49
  pointsStyle,
48
50
  raised,
49
51
  style,
@@ -85,7 +87,7 @@ const TilePure: FunctionComponent<Props> = ({
85
87
 
86
88
  {canShowPoints && (
87
89
  <span className={styles.points} style={pointsStyle}>
88
- {points}
90
+ {pointsFormatted}
89
91
  </span>
90
92
  )}
91
93
  </div>
@@ -2,7 +2,7 @@ import classNames from 'classnames';
2
2
  import { FunctionComponent } from 'react';
3
3
 
4
4
  import { Check, Cross } from 'icons';
5
- import { selectVerify, useTranslate, useTypedSelector } from 'state';
5
+ import { selectLocale, selectVerify, useTranslate, useTypedSelector } from 'state';
6
6
 
7
7
  import Badge from '../Badge';
8
8
  import Sidebar from '../Sidebar';
@@ -17,6 +17,7 @@ interface Props {
17
17
 
18
18
  const Words: FunctionComponent<Props> = ({ className, isOpen, onClose }) => {
19
19
  const translate = useTranslate();
20
+ const locale = useTypedSelector(selectLocale);
20
21
  const { invalidWords, validWords } = useTypedSelector(selectVerify);
21
22
 
22
23
  return (
@@ -25,7 +26,7 @@ const Words: FunctionComponent<Props> = ({ className, isOpen, onClose }) => {
25
26
  title={
26
27
  <span className={styles.title}>
27
28
  <span>{translate('words.invalid')}</span>
28
- <Badge className={styles.badge}>{invalidWords.length}</Badge>
29
+ <Badge className={styles.badge}>{invalidWords.length.toLocaleString(locale)}</Badge>
29
30
  </span>
30
31
  }
31
32
  >
@@ -40,7 +41,7 @@ const Words: FunctionComponent<Props> = ({ className, isOpen, onClose }) => {
40
41
  title={
41
42
  <span className={styles.title}>
42
43
  <span>{translate('words.valid')}</span>
43
- <Badge className={styles.badge}>{validWords.length}</Badge>
44
+ <Badge className={styles.badge}>{validWords.length.toLocaleString(locale)}</Badge>
44
45
  </span>
45
46
  }
46
47
  >
@@ -23,6 +23,7 @@ export { default as Sidebar } from './Sidebar';
23
23
  export { default as Splash } from './Splash';
24
24
  export { default as SquareButton } from './SquareButton';
25
25
  export { default as SvgFontCss } from './SvgFontCss';
26
+ export { default as SvgFontFix } from './SvgFontFix';
26
27
  export { default as Tile } from './Tile';
27
28
  export { useTooltip } from './Tooltip';
28
29
  export { default as Well } from './Well';
@@ -1,4 +1,6 @@
1
+ export { default as useDirection } from './useDirection';
1
2
  export { default as useIsTablet } from './useIsTablet';
3
+ export { default as useLanguage } from './useLanguage';
2
4
  export { default as useLocalStorage } from './useLocalStorage';
3
5
  export { default as usePortal } from './usePortal';
4
6
  export { default as useUniqueId } from './useUniqueId';
@@ -0,0 +1,22 @@
1
+ import { useLayoutEffect } from 'react';
2
+
3
+ import { noop } from 'lib';
4
+
5
+ const useDirection = (direction: 'ltr' | 'rtl') => {
6
+ useLayoutEffect(() => {
7
+ const html = document.body.parentElement;
8
+
9
+ if (!html) {
10
+ return noop;
11
+ }
12
+
13
+ const old = html.dir;
14
+ html.dir = direction;
15
+
16
+ return () => {
17
+ html.dir = old;
18
+ };
19
+ }, [direction]);
20
+ };
21
+
22
+ export default useDirection;
@@ -0,0 +1,22 @@
1
+ import { useLayoutEffect } from 'react';
2
+
3
+ import { noop } from 'lib';
4
+
5
+ const useLanguage = (language: string) => {
6
+ useLayoutEffect(() => {
7
+ const html = document.body.parentElement;
8
+
9
+ if (!html) {
10
+ return noop;
11
+ }
12
+
13
+ const old = html.lang;
14
+ html.lang = language;
15
+
16
+ return () => {
17
+ html.lang = old;
18
+ };
19
+ }, [language]);
20
+ };
21
+
22
+ export default useLanguage;
@@ -0,0 +1,53 @@
1
+ import { Locale } from '@scrabble-solver/types';
2
+
3
+ interface LocaleFeatures {
4
+ direction: 'ltr' | 'rtl';
5
+ fontFamily: string;
6
+ consonants: boolean;
7
+ vowels: boolean;
8
+ }
9
+
10
+ export const LOCALE_FEATURES: Record<Locale, LocaleFeatures> = {
11
+ [Locale.DE_DE]: {
12
+ direction: 'ltr',
13
+ fontFamily: 'Open Sans',
14
+ consonants: true,
15
+ vowels: true,
16
+ },
17
+ [Locale.EN_GB]: {
18
+ direction: 'ltr',
19
+ fontFamily: 'Open Sans',
20
+ consonants: true,
21
+ vowels: true,
22
+ },
23
+ [Locale.EN_US]: {
24
+ direction: 'ltr',
25
+ fontFamily: 'Open Sans',
26
+ consonants: true,
27
+ vowels: true,
28
+ },
29
+ [Locale.ES_ES]: {
30
+ direction: 'ltr',
31
+ fontFamily: 'Open Sans',
32
+ consonants: true,
33
+ vowels: true,
34
+ },
35
+ [Locale.FA_IR]: {
36
+ direction: 'rtl',
37
+ fontFamily: 'Vazirmatn',
38
+ consonants: false,
39
+ vowels: false,
40
+ },
41
+ [Locale.FR_FR]: {
42
+ direction: 'ltr',
43
+ fontFamily: 'Open Sans',
44
+ consonants: true,
45
+ vowels: true,
46
+ },
47
+ [Locale.PL_PL]: {
48
+ direction: 'ltr',
49
+ fontFamily: 'Open Sans',
50
+ consonants: true,
51
+ vowels: true,
52
+ },
53
+ };