@jobber/components-native 0.112.0 → 0.113.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/dist/docs/InputNumber/InputNumber.md +400 -10
- package/dist/package.json +5 -4
- package/dist/src/hooks/useAtlantisI18n/useAtlantisI18n.js +2 -4
- package/dist/src/hooks/useAtlantisI18n/useAtlantisI18n.test.js +15 -115
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/src/hooks/useAtlantisI18n/useAtlantisI18n.d.ts +1 -3
- package/package.json +5 -4
- package/src/hooks/useAtlantisI18n/useAtlantisI18n.test.ts +25 -202
- package/src/hooks/useAtlantisI18n/useAtlantisI18n.ts +3 -13
- package/dist/src/hooks/useAtlantisI18n/releasedLanguages.js +0 -21
- package/dist/types/src/hooks/useAtlantisI18n/releasedLanguages.d.ts +0 -18
- package/src/hooks/useAtlantisI18n/releasedLanguages.ts +0 -23
|
@@ -1,20 +1,410 @@
|
|
|
1
1
|
# InputNumber
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Summary
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`InputNumber` collects a single numeric value in a form. Reach for it when the
|
|
6
|
+
value benefits from being nudged up or down, such as quantities, prices,
|
|
7
|
+
durations, or counts.
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
Most fields only need the props shown below. For rare layouts the props can't
|
|
10
|
+
handle, you can build the same field from smaller pieces. See the **Implement**
|
|
11
|
+
tab.
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
using a number input. For example, phone numbers and credit card numbers provide
|
|
12
|
-
no value to the user by offering an incrementer.
|
|
13
|
+
### When to use
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
* The value is a number the user increments or decrements, like a quantity,
|
|
16
|
+
price, day count, or number of repetitions
|
|
17
|
+
* A stepper, min/max bounds, or number formatting would help the user
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
### When not to use
|
|
20
|
+
|
|
21
|
+
* The value is a sequence of digits that is never calculated with, such as phone
|
|
22
|
+
numbers, credit-card numbers, or postal codes. A stepper adds no value there;
|
|
23
|
+
use [InputText](../InputText/InputText.md) instead.
|
|
24
|
+
|
|
25
|
+
## Anatomy
|
|
26
|
+
|
|
27
|
+
`InputNumber` typically includes:
|
|
28
|
+
|
|
29
|
+
* Label (required): names the value the field collects
|
|
30
|
+
* Value field (required): the number the user types or steps through
|
|
31
|
+
* Stepper (optional): increment and decrement controls that change the value by
|
|
32
|
+
`step`
|
|
33
|
+
* Prefix or suffix (optional): a unit or symbol shown alongside the value, like
|
|
34
|
+
$ or kg
|
|
35
|
+
* Loading indicator (optional): replaces the stepper while background work runs
|
|
36
|
+
|
|
37
|
+
## Behavior
|
|
38
|
+
|
|
39
|
+
* The field is controlled through `value`, where a number sets the value and
|
|
40
|
+
`null` leaves it empty.
|
|
41
|
+
* `onValueCommitted` fires when the user commits a value: on blur, on Enter, or
|
|
42
|
+
when they use the stepper or arrow keys. Use `onValueChange` for per-keystroke
|
|
43
|
+
updates.
|
|
44
|
+
* The stepper buttons and the Up and Down arrow keys change the value by `step`
|
|
45
|
+
(default 1).
|
|
46
|
+
* `min` and `max` bound the value, and the stepper stops at each limit.
|
|
47
|
+
* `loading` hides the stepper and shows an indicator in its place. The field
|
|
48
|
+
stays editable, so use `readOnly` or `disabled` to lock it.
|
|
49
|
+
|
|
50
|
+
## Options
|
|
51
|
+
|
|
52
|
+
### Basic
|
|
53
|
+
|
|
54
|
+
Pass `label`, a controlled `value`, and `onValueCommitted`. Bounds (`min` /
|
|
55
|
+
`max`) and `step` are optional.
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import React, { useState } from "react";
|
|
59
|
+
import type { InputNumberProps } from "@jobber/components";
|
|
60
|
+
import { InputNumber } from "@jobber/components";
|
|
61
|
+
|
|
62
|
+
export function InputNumberBasicExample(props: Partial<InputNumberProps>) {
|
|
63
|
+
const [value, setValue] = useState<number | null>(3);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<InputNumber
|
|
67
|
+
label="Quantity"
|
|
68
|
+
min={0}
|
|
69
|
+
max={100}
|
|
70
|
+
{...props}
|
|
71
|
+
value={value}
|
|
72
|
+
onValueCommitted={setValue}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Prefixes and suffixes
|
|
79
|
+
|
|
80
|
+
Use `prefix` or `suffix` to add a unit or symbol. A suffix can be a label, an
|
|
81
|
+
icon, or a clickable icon that runs an action (give it an `ariaLabel`).
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import React, { useState } from "react";
|
|
85
|
+
import { InputNumber } from "@jobber/components";
|
|
86
|
+
import { Content } from "@jobber/components/Content";
|
|
87
|
+
|
|
88
|
+
export function InputNumberAffixesExample() {
|
|
89
|
+
const [price, setPrice] = useState<number | null>(42);
|
|
90
|
+
const [days, setDays] = useState<number | null>(7);
|
|
91
|
+
const [reps, setReps] = useState<number | null>(3);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<Content>
|
|
95
|
+
<InputNumber
|
|
96
|
+
label="Price"
|
|
97
|
+
prefix={{ label: "$" }}
|
|
98
|
+
suffix={{ label: "USD" }}
|
|
99
|
+
value={price}
|
|
100
|
+
onValueCommitted={setPrice}
|
|
101
|
+
/>
|
|
102
|
+
|
|
103
|
+
<InputNumber
|
|
104
|
+
label="Follow-up in"
|
|
105
|
+
suffix={{ icon: "calendar", label: "days" }}
|
|
106
|
+
value={days}
|
|
107
|
+
onValueCommitted={setDays}
|
|
108
|
+
/>
|
|
109
|
+
|
|
110
|
+
<InputNumber
|
|
111
|
+
label="Repetitions"
|
|
112
|
+
suffix={{
|
|
113
|
+
icon: "cross",
|
|
114
|
+
ariaLabel: "Clear value",
|
|
115
|
+
onClick: () => setReps(null),
|
|
116
|
+
}}
|
|
117
|
+
value={reps}
|
|
118
|
+
onValueCommitted={setReps}
|
|
119
|
+
/>
|
|
120
|
+
</Content>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Sizes
|
|
126
|
+
|
|
127
|
+
3 sizes are available. `default` fits almost every form; use `small` only in
|
|
128
|
+
tight spaces and `large` only in especially spacious layouts.
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import React, { useState } from "react";
|
|
132
|
+
import { InputNumber } from "@jobber/components";
|
|
133
|
+
import { Content } from "@jobber/components/Content";
|
|
134
|
+
|
|
135
|
+
export function InputNumberSizesExample() {
|
|
136
|
+
const [small, setSmall] = useState<number | null>(42);
|
|
137
|
+
const [base, setBase] = useState<number | null>(42);
|
|
138
|
+
const [large, setLarge] = useState<number | null>(42);
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<Content>
|
|
142
|
+
<InputNumber
|
|
143
|
+
label="Small"
|
|
144
|
+
size="small"
|
|
145
|
+
suffix={{ label: "items" }}
|
|
146
|
+
value={small}
|
|
147
|
+
onValueCommitted={setSmall}
|
|
148
|
+
/>
|
|
149
|
+
<InputNumber
|
|
150
|
+
label="Default"
|
|
151
|
+
size="default"
|
|
152
|
+
suffix={{ label: "items" }}
|
|
153
|
+
value={base}
|
|
154
|
+
onValueCommitted={setBase}
|
|
155
|
+
/>
|
|
156
|
+
<InputNumber
|
|
157
|
+
label="Large"
|
|
158
|
+
size="large"
|
|
159
|
+
suffix={{ label: "items" }}
|
|
160
|
+
value={large}
|
|
161
|
+
onValueCommitted={setLarge}
|
|
162
|
+
/>
|
|
163
|
+
</Content>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Formatting
|
|
169
|
+
|
|
170
|
+
`format` takes any `Intl.NumberFormatOptions` and controls only how the value is
|
|
171
|
+
displayed; the committed value stays a plain number. See the **Implement** tab
|
|
172
|
+
for how percent and currency values map to the underlying number.
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
import React, { useState } from "react";
|
|
176
|
+
import { InputNumber } from "@jobber/components";
|
|
177
|
+
import { Content } from "@jobber/components/Content";
|
|
178
|
+
|
|
179
|
+
export function InputNumberFormattingExample() {
|
|
180
|
+
const [currency, setCurrency] = useState<number | null>(1234.5);
|
|
181
|
+
const [percent, setPercent] = useState<number | null>(0.5);
|
|
182
|
+
const [decimal, setDecimal] = useState<number | null>(11.13);
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<Content>
|
|
186
|
+
<InputNumber
|
|
187
|
+
label="Currency"
|
|
188
|
+
description='{ style: "currency", currency: "USD" }'
|
|
189
|
+
format={{ style: "currency", currency: "USD" }}
|
|
190
|
+
value={currency}
|
|
191
|
+
onValueCommitted={setCurrency}
|
|
192
|
+
/>
|
|
193
|
+
<InputNumber
|
|
194
|
+
label="Percent"
|
|
195
|
+
description='{ style: "percent" } — value is a ratio: 0.5 → 50%'
|
|
196
|
+
format={{ style: "percent", maximumFractionDigits: 2 }}
|
|
197
|
+
value={percent}
|
|
198
|
+
onValueCommitted={setPercent}
|
|
199
|
+
/>
|
|
200
|
+
<InputNumber
|
|
201
|
+
label="Decimal"
|
|
202
|
+
description="{ maximumFractionDigits: 2 }"
|
|
203
|
+
format={{ maximumFractionDigits: 2 }}
|
|
204
|
+
value={decimal}
|
|
205
|
+
onValueCommitted={setDecimal}
|
|
206
|
+
/>
|
|
207
|
+
</Content>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Loading
|
|
213
|
+
|
|
214
|
+
`loading` shows a non-blocking indicator in the stepper's slot for background
|
|
215
|
+
work, like saving. The field stays editable and the stepper is hidden while
|
|
216
|
+
loading.
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
import React, { useState } from "react";
|
|
220
|
+
import { InputNumber } from "@jobber/components";
|
|
221
|
+
|
|
222
|
+
export function InputNumberLoadingExample() {
|
|
223
|
+
const [value, setValue] = useState<number | null>(42);
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<InputNumber
|
|
227
|
+
loading
|
|
228
|
+
label="Quantity"
|
|
229
|
+
suffix={{ label: "items" }}
|
|
230
|
+
value={value}
|
|
231
|
+
onValueCommitted={setValue}
|
|
232
|
+
/>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Content guidelines
|
|
238
|
+
|
|
239
|
+
### Label the unit, don't repeat it
|
|
240
|
+
|
|
241
|
+
Put the unit in the label or an affix, not both.
|
|
242
|
+
|
|
243
|
+
| ✅ Do | ❌ Don't |
|
|
244
|
+
| ------------------------------- | --------------------------------------- |
|
|
245
|
+
| Label "Weight", suffix "kg" | Label "Weight (kg)", suffix "kg" |
|
|
246
|
+
| Label "Duration", suffix "days" | Label "Duration in days", suffix "days" |
|
|
247
|
+
|
|
248
|
+
### Keep labels short and sentence case
|
|
249
|
+
|
|
250
|
+
| ✅ Do | ❌ Don't |
|
|
251
|
+
| -------- | ----------------------- |
|
|
252
|
+
| Quantity | Enter the quantity here |
|
|
253
|
+
| Discount | DISCOUNT % |
|
|
254
|
+
|
|
255
|
+
### Put the symbol where it's read
|
|
256
|
+
|
|
257
|
+
Use a prefix for a leading symbol and a suffix for a trailing unit, matching how
|
|
258
|
+
the value is spoken.
|
|
259
|
+
|
|
260
|
+
| ✅ Do | ❌ Don't |
|
|
261
|
+
| -------------------- | -------------------- |
|
|
262
|
+
| Prefix "$", value 40 | Suffix "$", value 40 |
|
|
263
|
+
| Suffix "%", value 15 | Prefix "%", value 15 |
|
|
264
|
+
|
|
265
|
+
### Keep validation errors helpful
|
|
266
|
+
|
|
267
|
+
When a value breaks `min` or `max`, provide helpful guidance on what values will
|
|
268
|
+
be accepted as opposed to just providing a generic error.
|
|
269
|
+
|
|
270
|
+
| ✅ Do | ❌ Don't |
|
|
271
|
+
| ------------------------------ | ------------- |
|
|
272
|
+
| Enter a value between 1 and 99 | Invalid input |
|
|
273
|
+
| Quantity can't be more than 50 | Error |
|
|
274
|
+
|
|
275
|
+
### Use numbers as opposed to spelling them
|
|
276
|
+
|
|
277
|
+
Use numerals in labels, helper text, affixes, and bounds.
|
|
278
|
+
|
|
279
|
+
| ✅ Do | ❌ Don't |
|
|
280
|
+
| ----------- | --------------- |
|
|
281
|
+
| Max 3 items | Max three items |
|
|
282
|
+
|
|
283
|
+
## Do's and Don'ts
|
|
284
|
+
|
|
285
|
+
#### Do:
|
|
286
|
+
|
|
287
|
+
* ✅ Use for values the user increments or decrements
|
|
288
|
+
* ✅ Set `min` and `max` when the value has real bounds
|
|
289
|
+
* ✅ Use `format` for currency, percent, and decimals rather than formatting the
|
|
290
|
+
value yourself
|
|
291
|
+
* ✅ Use `loading` for background work so the field stays usable
|
|
292
|
+
|
|
293
|
+
#### Don't:
|
|
294
|
+
|
|
295
|
+
* ❌ Use it for digit sequences that are never calculated with, like phone or
|
|
296
|
+
credit card numbers
|
|
297
|
+
* ❌ Disable the field to communicate an error; show an `error` message instead
|
|
298
|
+
* ❌ Repeat the unit in both the label and an affix
|
|
299
|
+
|
|
300
|
+
## Accessibility notes
|
|
301
|
+
|
|
302
|
+
The field is a native number input, so it is reachable and operable by keyboard
|
|
303
|
+
and assistive technology.
|
|
304
|
+
|
|
305
|
+
| Key | Behavior |
|
|
306
|
+
| ---------------- | ------------------------------- |
|
|
307
|
+
| Tab | Moves focus to the field |
|
|
308
|
+
| Up / Down arrows | Increment / decrement by `step` |
|
|
309
|
+
| Enter | Commits the current value |
|
|
310
|
+
| Type | Replaces the value |
|
|
311
|
+
|
|
312
|
+
Give a clickable affix a clear `ariaLabel` describing its action, like "Clear
|
|
313
|
+
value".
|
|
314
|
+
|
|
315
|
+
## Related components
|
|
316
|
+
|
|
317
|
+
* For digit sequences that are not calculated with, like phone or credit card
|
|
318
|
+
numbers, use [InputText](../InputText/InputText.md).
|
|
319
|
+
* For dates, use [InputDate](../InputDate/InputDate.md).
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
## Anatomy
|
|
323
|
+
|
|
324
|
+
The prop-driven `<InputNumber>` composes a set of parts. You only need these
|
|
325
|
+
when the props can't express a layout; otherwise reach for the props shown on
|
|
326
|
+
the **Design** tab.
|
|
327
|
+
|
|
328
|
+
| Part | Description |
|
|
329
|
+
| ----------------------- | -------------------------------------------------------------------- |
|
|
330
|
+
| `Wrapper` | Owns the field configuration and state; provides it to the parts |
|
|
331
|
+
| `Group` | The bordered field row |
|
|
332
|
+
| `Input` | The input area; holds the `Label` and the `Stepper` / `Loading` slot |
|
|
333
|
+
| `Label` | Floating field label |
|
|
334
|
+
| `Stepper` | The increment / decrement button pair |
|
|
335
|
+
| `Increment` `Decrement` | The individual stepper buttons |
|
|
336
|
+
| `Affix` | Prefix / suffix content (label, icon, or clickable icon) |
|
|
337
|
+
| `Loading` | Non-blocking loading indicator slot |
|
|
338
|
+
| `Footer` | Below-field row that holds `Description` and `Error` |
|
|
339
|
+
| `Description` | Helper text below the field |
|
|
340
|
+
| `Error` | Styled error message below the field |
|
|
341
|
+
|
|
342
|
+
## Composition
|
|
343
|
+
|
|
344
|
+
The prop-driven component is sugar: it renders exactly the tree you would write
|
|
345
|
+
by hand with `<InputNumber.Wrapper>` and the parts. To customize a single piece,
|
|
346
|
+
compose the tree yourself and swap that one part — the other parts keep their
|
|
347
|
+
defaults. The sugar does not merge consumer-provided parts into its render, so
|
|
348
|
+
there is no per-slot precedence to reason about.
|
|
349
|
+
|
|
350
|
+
`Wrapper` owns the field state and shares it with the parts through context, so
|
|
351
|
+
every part must be rendered inside a `Wrapper` (a part used outside one throws).
|
|
352
|
+
|
|
353
|
+
The example below replaces the default stepper icons with `+` / `−` and leaves
|
|
354
|
+
everything else as the default:
|
|
355
|
+
|
|
356
|
+
```tsx
|
|
357
|
+
import React, { useState } from "react";
|
|
358
|
+
import { InputNumber } from "@jobber/components";
|
|
359
|
+
|
|
360
|
+
export function InputNumberCompositionExample() {
|
|
361
|
+
const [value, setValue] = useState<number | null>(3);
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<InputNumber.Wrapper value={value} onValueCommitted={setValue}>
|
|
365
|
+
<InputNumber.Group>
|
|
366
|
+
<InputNumber.Input>
|
|
367
|
+
<InputNumber.Label>Quantity</InputNumber.Label>
|
|
368
|
+
<InputNumber.Stepper>
|
|
369
|
+
<InputNumber.Increment ariaLabel="Increase Quantity">
|
|
370
|
+
+
|
|
371
|
+
</InputNumber.Increment>
|
|
372
|
+
<InputNumber.Decrement ariaLabel="Decrease Quantity">
|
|
373
|
+
−
|
|
374
|
+
</InputNumber.Decrement>
|
|
375
|
+
</InputNumber.Stepper>
|
|
376
|
+
</InputNumber.Input>
|
|
377
|
+
</InputNumber.Group>
|
|
378
|
+
</InputNumber.Wrapper>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Controlled usage
|
|
384
|
+
|
|
385
|
+
The field is controlled: pass `value` (a `number`, or `null` for empty) and read
|
|
386
|
+
changes back through one of two callbacks.
|
|
387
|
+
|
|
388
|
+
| Callback | Fires | Use for |
|
|
389
|
+
| ------------------ | ----------------------------------------------------------- | ---------------------------------- |
|
|
390
|
+
| `onValueChange` | On every parsed change (typing, paste, stepper, arrow step) | Live-updating UI as the user types |
|
|
391
|
+
| `onValueCommitted` | When the user commits (blur, Enter, stepper, arrow step) | Saving / validating a final value |
|
|
392
|
+
|
|
393
|
+
Both emit `null` when the field is empty. Prefer `onValueCommitted` for
|
|
394
|
+
persistence so you are not writing on every keystroke.
|
|
395
|
+
|
|
396
|
+
## Formatting semantics
|
|
397
|
+
|
|
398
|
+
`format` is forwarded to Base UI's `NumberField` `format` and accepts any
|
|
399
|
+
`Intl.NumberFormatOptions`. It changes the display only; the committed value is
|
|
400
|
+
always a plain number. Two things to know:
|
|
401
|
+
|
|
402
|
+
* **Percent** (`{ style: "percent" }`) treats the value as a ratio: `0.5`
|
|
403
|
+
renders `50%`, and the stepper moves in ratio units. If you want the value to
|
|
404
|
+
be the number itself (`50` → `50%`), use `{ style: "unit", unit: "percent" }`.
|
|
405
|
+
* With no `format`, typed decimals are preserved (up to 12 fractional digits)
|
|
406
|
+
and thousands grouping follows the locale default, so `1234.5` renders as
|
|
407
|
+
`1,234.5`. Pass `{ useGrouping: false }` to render without separators.
|
|
18
408
|
|
|
19
409
|
|
|
20
410
|
## Props
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobber/components-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.113.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React Native implementation of Atlantis",
|
|
6
6
|
"repository": {
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
"compile": "tsc -p tsconfig.build.json",
|
|
54
54
|
"build:clean": "rm -rf ./dist",
|
|
55
55
|
"storybook": "storybook dev -p 6008 --disable-telemetry",
|
|
56
|
-
"storybook:build": "storybook build --disable-telemetry"
|
|
56
|
+
"storybook:build": "storybook build --disable-telemetry",
|
|
57
|
+
"lint:locales": "node scripts/check-locale-parity.mjs"
|
|
57
58
|
},
|
|
58
59
|
"dependencies": {
|
|
59
60
|
"@react-native-clipboard/clipboard": "^1.11.2",
|
|
@@ -74,7 +75,7 @@
|
|
|
74
75
|
"@babel/runtime": "^7.29.2",
|
|
75
76
|
"@gorhom/bottom-sheet": "^5.2.8",
|
|
76
77
|
"@jobber/design": "0.111.0",
|
|
77
|
-
"@jobber/hooks": "2.21.
|
|
78
|
+
"@jobber/hooks": "2.21.1",
|
|
78
79
|
"@react-native-community/datetimepicker": "^8.4.5",
|
|
79
80
|
"@react-native/babel-preset": "^0.82.1",
|
|
80
81
|
"@storybook/addon-a11y": "10.3.5",
|
|
@@ -124,5 +125,5 @@
|
|
|
124
125
|
"react-native-screens": ">=4.18.0",
|
|
125
126
|
"react-native-svg": ">=12.0.0"
|
|
126
127
|
},
|
|
127
|
-
"gitHead": "
|
|
128
|
+
"gitHead": "25cd8815738e86b9f49311096e7d4ffd02d843b5"
|
|
128
129
|
}
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
2
|
import en from "./locales/en.json";
|
|
3
3
|
import es from "./locales/es.json";
|
|
4
|
-
import { getReleasedLocale } from "./releasedLanguages";
|
|
5
4
|
import { dateFormatter } from "./utils/dateFormatter";
|
|
6
5
|
import { useAtlantisContext } from "../../AtlantisContext";
|
|
7
6
|
export function useAtlantisI18n() {
|
|
8
|
-
const { locale
|
|
9
|
-
const locale = getReleasedLocale(contextLocale);
|
|
7
|
+
const { locale, dateFormat, timeFormat, timeZone } = useAtlantisContext();
|
|
10
8
|
const t = useCallback((messageKey, values) => formatMessage(messageKey, values, locale), [formatMessage, locale]);
|
|
11
9
|
const formatDate = useCallback((date) => dateFormatter(date, dateFormat, { locale, timeZone }), [dateFormatter, locale]);
|
|
12
10
|
const formatTime = useCallback((date) => dateFormatter(date, timeFormat, { locale, timeZone }), [dateFormatter, locale]);
|
|
13
11
|
return { locale, t, formatDate, formatTime };
|
|
14
12
|
}
|
|
15
13
|
function getLocalizedStrings(locale) {
|
|
16
|
-
switch (locale
|
|
14
|
+
switch (locale) {
|
|
17
15
|
case "es":
|
|
18
16
|
return es;
|
|
19
17
|
default:
|
|
@@ -2,15 +2,10 @@ import { renderHook } from "@testing-library/react-native";
|
|
|
2
2
|
import { useAtlantisI18n } from ".";
|
|
3
3
|
import en from "./locales/en.json";
|
|
4
4
|
import es from "./locales/es.json";
|
|
5
|
-
import * as releasedLanguages from "./releasedLanguages";
|
|
6
5
|
import * as context from "../../AtlantisContext";
|
|
7
6
|
jest.mock("../../AtlantisContext", () => (Object.assign({
|
|
8
7
|
// need to mark this as a module so that we can spy on it
|
|
9
8
|
__esModule: true }, jest.requireActual("../../AtlantisContext"))));
|
|
10
|
-
// Mock releasedLanguages so that jest.spyOn can reliably intercept calls from
|
|
11
|
-
// inside the hook. Without this, Babel may resolve the named import as a local
|
|
12
|
-
// binding at module load time, bypassing any spy set up afterwards.
|
|
13
|
-
jest.mock("./releasedLanguages", () => (Object.assign({ __esModule: true }, jest.requireActual("./releasedLanguages"))));
|
|
14
9
|
const spy = jest.spyOn(context, "useAtlantisContext");
|
|
15
10
|
const testDate = new Date("2020-01-01T00:00:00.000Z");
|
|
16
11
|
const dateAfterSpringForward = new Date("2020-04-10T00:00:00.000Z");
|
|
@@ -27,123 +22,18 @@ describe("useAtlantisI18n", () => {
|
|
|
27
22
|
const { result } = renderHook(useAtlantisI18n);
|
|
28
23
|
expect(result.current.t("FormatFile.preview", { item: "🔱" })).toBe("Preview 🔱");
|
|
29
24
|
});
|
|
30
|
-
// These tests bypass the release gate via a spy so we can validate Spanish
|
|
31
|
-
// string correctness as the feature is built, independently of RELEASED_LANGUAGES.
|
|
32
25
|
describe("Español", () => {
|
|
33
|
-
let getReleasedLocaleSpy;
|
|
34
|
-
beforeEach(() => {
|
|
35
|
-
getReleasedLocaleSpy = jest
|
|
36
|
-
.spyOn(releasedLanguages, "getReleasedLocale")
|
|
37
|
-
.mockImplementation(locale => locale);
|
|
38
|
-
});
|
|
39
|
-
afterEach(() => {
|
|
40
|
-
getReleasedLocaleSpy.mockRestore();
|
|
41
|
-
});
|
|
42
26
|
it("should return español", () => {
|
|
43
27
|
spy.mockReturnValueOnce(Object.assign(Object.assign({}, context.atlantisContextDefaultValues), { locale: "es" }));
|
|
44
28
|
const { result } = renderHook(useAtlantisI18n);
|
|
45
29
|
expect(result.current.t("cancel")).toBe("Cancelar");
|
|
46
30
|
});
|
|
47
|
-
it("should return español for regional variant es-US", () => {
|
|
48
|
-
spy.mockReturnValueOnce(Object.assign(Object.assign({}, context.atlantisContextDefaultValues), { locale: "es-US" }));
|
|
49
|
-
const { result } = renderHook(useAtlantisI18n);
|
|
50
|
-
expect(result.current.t("cancel")).toBe("Cancelar");
|
|
51
|
-
});
|
|
52
|
-
describe("formatDate", () => {
|
|
53
|
-
it("should return the date formatted for es", () => {
|
|
54
|
-
spy.mockReturnValueOnce(Object.assign(Object.assign({}, context.atlantisContextDefaultValues), { locale: "es" }));
|
|
55
|
-
const { result } = renderHook(useAtlantisI18n);
|
|
56
|
-
expect(result.current.formatDate(testDate)).toBe("1 ene 2020");
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
describe("formatTime", () => {
|
|
60
|
-
it("should return the time formatted for es", () => {
|
|
61
|
-
spy.mockReturnValueOnce(Object.assign(Object.assign({}, context.atlantisContextDefaultValues), { locale: "es" }));
|
|
62
|
-
const { result } = renderHook(useAtlantisI18n);
|
|
63
|
-
expect(result.current.formatTime(testDate)).toBe("00:00");
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
31
|
});
|
|
67
|
-
describe("
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
expect(result.current.locale).toBe("en");
|
|
73
|
-
});
|
|
74
|
-
it("should return english translations", () => {
|
|
75
|
-
spy.mockReturnValueOnce(Object.assign(Object.assign({}, context.atlantisContextDefaultValues), { locale: "fr" }));
|
|
76
|
-
const { result } = renderHook(useAtlantisI18n);
|
|
77
|
-
expect(result.current.t("cancel")).toBe("Cancel");
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
describe("es-US (unreleased locale variant)", () => {
|
|
81
|
-
it("should return 'en' as the locale", () => {
|
|
82
|
-
spy.mockReturnValueOnce(Object.assign(Object.assign({}, context.atlantisContextDefaultValues), { locale: "es-US" }));
|
|
83
|
-
const { result } = renderHook(useAtlantisI18n);
|
|
84
|
-
expect(result.current.locale).toBe("en");
|
|
85
|
-
});
|
|
86
|
-
it("should return english translations", () => {
|
|
87
|
-
spy.mockReturnValueOnce(Object.assign(Object.assign({}, context.atlantisContextDefaultValues), { locale: "es-US" }));
|
|
88
|
-
const { result } = renderHook(useAtlantisI18n);
|
|
89
|
-
expect(result.current.t("cancel")).toBe("Cancel");
|
|
90
|
-
});
|
|
91
|
-
it("should format date using english locale", () => {
|
|
92
|
-
spy.mockReturnValueOnce(Object.assign(Object.assign({}, context.atlantisContextDefaultValues), { locale: "es-US" }));
|
|
93
|
-
const { result } = renderHook(useAtlantisI18n);
|
|
94
|
-
expect(result.current.formatDate(testDate)).toBe("Jan 1, 2020");
|
|
95
|
-
});
|
|
96
|
-
it("should format time using english locale", () => {
|
|
97
|
-
spy.mockReturnValueOnce(Object.assign(Object.assign({}, context.atlantisContextDefaultValues), { locale: "es-US" }));
|
|
98
|
-
const { result } = renderHook(useAtlantisI18n);
|
|
99
|
-
expect(result.current.formatTime(testDate)).toBe("12:00 AM");
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
describe("es (not yet released)", () => {
|
|
103
|
-
it("should return 'en' as the locale", () => {
|
|
104
|
-
spy.mockReturnValueOnce(Object.assign(Object.assign({}, context.atlantisContextDefaultValues), { locale: "es" }));
|
|
105
|
-
const { result } = renderHook(useAtlantisI18n);
|
|
106
|
-
expect(result.current.locale).toBe("en");
|
|
107
|
-
});
|
|
108
|
-
it("should return english translations", () => {
|
|
109
|
-
spy.mockReturnValueOnce(Object.assign(Object.assign({}, context.atlantisContextDefaultValues), { locale: "es" }));
|
|
110
|
-
const { result } = renderHook(useAtlantisI18n);
|
|
111
|
-
expect(result.current.t("cancel")).toBe("Cancel");
|
|
112
|
-
});
|
|
113
|
-
it("should format date using english locale", () => {
|
|
114
|
-
spy.mockReturnValueOnce(Object.assign(Object.assign({}, context.atlantisContextDefaultValues), { locale: "es" }));
|
|
115
|
-
const { result } = renderHook(useAtlantisI18n);
|
|
116
|
-
expect(result.current.formatDate(testDate)).toBe("Jan 1, 2020");
|
|
117
|
-
});
|
|
118
|
-
it("should format time using english locale", () => {
|
|
119
|
-
spy.mockReturnValueOnce(Object.assign(Object.assign({}, context.atlantisContextDefaultValues), { locale: "es" }));
|
|
120
|
-
const { result } = renderHook(useAtlantisI18n);
|
|
121
|
-
expect(result.current.formatTime(testDate)).toBe("12:00 AM");
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
describe("en-CA (released language, regional variant)", () => {
|
|
125
|
-
it("should preserve the full locale for date formatting", () => {
|
|
126
|
-
spy.mockReturnValueOnce(Object.assign(Object.assign({}, context.atlantisContextDefaultValues), { locale: "en-CA" }));
|
|
127
|
-
const { result } = renderHook(useAtlantisI18n);
|
|
128
|
-
expect(result.current.locale).toBe("en-CA");
|
|
129
|
-
});
|
|
130
|
-
it("should return english translations", () => {
|
|
131
|
-
spy.mockReturnValueOnce(Object.assign(Object.assign({}, context.atlantisContextDefaultValues), { locale: "en-CA" }));
|
|
132
|
-
const { result } = renderHook(useAtlantisI18n);
|
|
133
|
-
expect(result.current.t("cancel")).toBe("Cancel");
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
describe("en-GB (released language, regional variant)", () => {
|
|
137
|
-
it("should preserve the full locale for date formatting", () => {
|
|
138
|
-
spy.mockReturnValueOnce(Object.assign(Object.assign({}, context.atlantisContextDefaultValues), { locale: "en-GB" }));
|
|
139
|
-
const { result } = renderHook(useAtlantisI18n);
|
|
140
|
-
expect(result.current.locale).toBe("en-GB");
|
|
141
|
-
});
|
|
142
|
-
it("should return english translations", () => {
|
|
143
|
-
spy.mockReturnValueOnce(Object.assign(Object.assign({}, context.atlantisContextDefaultValues), { locale: "en-GB" }));
|
|
144
|
-
const { result } = renderHook(useAtlantisI18n);
|
|
145
|
-
expect(result.current.t("cancel")).toBe("Cancel");
|
|
146
|
-
});
|
|
32
|
+
describe("Unsupported language", () => {
|
|
33
|
+
it("should return the english translation", () => {
|
|
34
|
+
spy.mockReturnValueOnce(Object.assign(Object.assign({}, context.atlantisContextDefaultValues), { locale: "fr" }));
|
|
35
|
+
const { result } = renderHook(useAtlantisI18n);
|
|
36
|
+
expect(result.current.t("cancel")).toBe("Cancel");
|
|
147
37
|
});
|
|
148
38
|
});
|
|
149
39
|
describe("Translation files", () => {
|
|
@@ -156,6 +46,11 @@ describe("useAtlantisI18n", () => {
|
|
|
156
46
|
const { result } = renderHook(useAtlantisI18n);
|
|
157
47
|
expect(result.current.formatDate(testDate)).toBe("Jan 1, 2020");
|
|
158
48
|
});
|
|
49
|
+
it("should return the date formatted for es", () => {
|
|
50
|
+
spy.mockReturnValueOnce(Object.assign(Object.assign({}, context.atlantisContextDefaultValues), { locale: "es" }));
|
|
51
|
+
const { result } = renderHook(useAtlantisI18n);
|
|
52
|
+
expect(result.current.formatDate(testDate)).toBe("1 ene 2020");
|
|
53
|
+
});
|
|
159
54
|
describe("Timezone", () => {
|
|
160
55
|
it.each([
|
|
161
56
|
["America/New_York", "Dec 31, 2019"],
|
|
@@ -175,6 +70,11 @@ describe("useAtlantisI18n", () => {
|
|
|
175
70
|
const { result } = renderHook(useAtlantisI18n);
|
|
176
71
|
expect(result.current.formatTime(testDate)).toBe("12:00 AM");
|
|
177
72
|
});
|
|
73
|
+
it("should return the time formatted for es", () => {
|
|
74
|
+
spy.mockReturnValueOnce(Object.assign(Object.assign({}, context.atlantisContextDefaultValues), { locale: "es" }));
|
|
75
|
+
const { result } = renderHook(useAtlantisI18n);
|
|
76
|
+
expect(result.current.formatTime(testDate)).toBe("00:00");
|
|
77
|
+
});
|
|
178
78
|
describe("Timezone", () => {
|
|
179
79
|
it.each([
|
|
180
80
|
["America/New_York", "7:00 PM"],
|