@acvl/mobile-components 0.0.4 → 0.0.6

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 (123) hide show
  1. package/README.md +15 -1
  2. package/lib/module/camara/CampoDeFotos.js +4 -2
  3. package/lib/module/camara/CampoDeFotos.js.map +1 -1
  4. package/lib/module/camara/elegir.js +4 -3
  5. package/lib/module/camara/elegir.js.map +1 -1
  6. package/lib/module/componentes/AccionesDelEncabezado.js +3 -1
  7. package/lib/module/componentes/AccionesDelEncabezado.js.map +1 -1
  8. package/lib/module/componentes/BotonDeAvatar.js +24 -20
  9. package/lib/module/componentes/BotonDeAvatar.js.map +1 -1
  10. package/lib/module/componentes/BotonFlotante.js +71 -0
  11. package/lib/module/componentes/BotonFlotante.js.map +1 -0
  12. package/lib/module/componentes/CabeceraEmpujada.js +3 -1
  13. package/lib/module/componentes/CabeceraEmpujada.js.map +1 -1
  14. package/lib/module/componentes/PantallaDeError.js +6 -4
  15. package/lib/module/componentes/PantallaDeError.js.map +1 -1
  16. package/lib/module/componentes/TiraDeFotos.js +4 -2
  17. package/lib/module/componentes/TiraDeFotos.js.map +1 -1
  18. package/lib/module/componentes/VisorDeFoto.js +6 -4
  19. package/lib/module/componentes/VisorDeFoto.js.map +1 -1
  20. package/lib/module/dialogos/HostDeDialogos.js +3 -1
  21. package/lib/module/dialogos/HostDeDialogos.js.map +1 -1
  22. package/lib/module/dialogos/dialogos.js +6 -4
  23. package/lib/module/dialogos/dialogos.js.map +1 -1
  24. package/lib/module/formularios/SelectorDeOpcion.js +13 -7
  25. package/lib/module/formularios/SelectorDeOpcion.js.map +1 -1
  26. package/lib/module/formularios/campos.js +9 -4
  27. package/lib/module/formularios/campos.js.map +1 -1
  28. package/lib/module/formularios/guardado.js +4 -2
  29. package/lib/module/formularios/guardado.js.map +1 -1
  30. package/lib/module/fotos/elegir.js +10 -8
  31. package/lib/module/fotos/elegir.js.map +1 -1
  32. package/lib/module/hoja/HojaDeLista.js +5 -3
  33. package/lib/module/hoja/HojaDeLista.js.map +1 -1
  34. package/lib/module/hoja/useMasDeLaFicha.js +3 -1
  35. package/lib/module/hoja/useMasDeLaFicha.js.map +1 -1
  36. package/lib/module/idioma/ProveedorDeIdioma.js +54 -0
  37. package/lib/module/idioma/ProveedorDeIdioma.js.map +1 -0
  38. package/lib/module/idioma/index.js +4 -0
  39. package/lib/module/idioma/index.js.map +1 -0
  40. package/lib/module/idioma/textos.js +237 -0
  41. package/lib/module/idioma/textos.js.map +1 -0
  42. package/lib/module/index.js +5 -0
  43. package/lib/module/index.js.map +1 -1
  44. package/lib/module/listas/HojaDeFiltros.js +11 -8
  45. package/lib/module/listas/HojaDeFiltros.js.map +1 -1
  46. package/lib/module/listas/ListaPaginada.js +6 -4
  47. package/lib/module/listas/ListaPaginada.js.map +1 -1
  48. package/lib/module/listas/piezas.js +13 -7
  49. package/lib/module/listas/piezas.js.map +1 -1
  50. package/lib/module/pantallas/PantallaDeAcceso.js +6 -4
  51. package/lib/module/pantallas/PantallaDeAcceso.js.map +1 -1
  52. package/lib/module/pantallas/PantallaDeEdicion.js +9 -6
  53. package/lib/module/pantallas/PantallaDeEdicion.js.map +1 -1
  54. package/lib/module/pantallas/PantallaDeFicha.js +7 -5
  55. package/lib/module/pantallas/PantallaDeFicha.js.map +1 -1
  56. package/lib/module/pantallas/PantallaDeHistorial.js +18 -14
  57. package/lib/module/pantallas/PantallaDeHistorial.js.map +1 -1
  58. package/lib/module/pantallas/PantallaDePerfil.js +12 -10
  59. package/lib/module/pantallas/PantallaDePerfil.js.map +1 -1
  60. package/lib/typescript/src/camara/CampoDeFotos.d.ts.map +1 -1
  61. package/lib/typescript/src/camara/elegir.d.ts.map +1 -1
  62. package/lib/typescript/src/componentes/AccionesDelEncabezado.d.ts.map +1 -1
  63. package/lib/typescript/src/componentes/BotonDeAvatar.d.ts.map +1 -1
  64. package/lib/typescript/src/componentes/BotonFlotante.d.ts +21 -0
  65. package/lib/typescript/src/componentes/BotonFlotante.d.ts.map +1 -0
  66. package/lib/typescript/src/componentes/CabeceraEmpujada.d.ts.map +1 -1
  67. package/lib/typescript/src/componentes/PantallaDeError.d.ts.map +1 -1
  68. package/lib/typescript/src/componentes/TiraDeFotos.d.ts.map +1 -1
  69. package/lib/typescript/src/componentes/VisorDeFoto.d.ts.map +1 -1
  70. package/lib/typescript/src/dialogos/HostDeDialogos.d.ts.map +1 -1
  71. package/lib/typescript/src/dialogos/dialogos.d.ts.map +1 -1
  72. package/lib/typescript/src/formularios/SelectorDeOpcion.d.ts.map +1 -1
  73. package/lib/typescript/src/formularios/campos.d.ts.map +1 -1
  74. package/lib/typescript/src/formularios/guardado.d.ts.map +1 -1
  75. package/lib/typescript/src/fotos/elegir.d.ts.map +1 -1
  76. package/lib/typescript/src/hoja/HojaDeLista.d.ts.map +1 -1
  77. package/lib/typescript/src/hoja/useMasDeLaFicha.d.ts.map +1 -1
  78. package/lib/typescript/src/idioma/ProveedorDeIdioma.d.ts +16 -0
  79. package/lib/typescript/src/idioma/ProveedorDeIdioma.d.ts.map +1 -0
  80. package/lib/typescript/src/idioma/index.d.ts +3 -0
  81. package/lib/typescript/src/idioma/index.d.ts.map +1 -0
  82. package/lib/typescript/src/idioma/textos.d.ts +124 -0
  83. package/lib/typescript/src/idioma/textos.d.ts.map +1 -0
  84. package/lib/typescript/src/index.d.ts +2 -0
  85. package/lib/typescript/src/index.d.ts.map +1 -1
  86. package/lib/typescript/src/listas/HojaDeFiltros.d.ts.map +1 -1
  87. package/lib/typescript/src/listas/ListaPaginada.d.ts.map +1 -1
  88. package/lib/typescript/src/listas/piezas.d.ts.map +1 -1
  89. package/lib/typescript/src/pantallas/PantallaDeAcceso.d.ts.map +1 -1
  90. package/lib/typescript/src/pantallas/PantallaDeEdicion.d.ts.map +1 -1
  91. package/lib/typescript/src/pantallas/PantallaDeFicha.d.ts.map +1 -1
  92. package/lib/typescript/src/pantallas/PantallaDeHistorial.d.ts.map +1 -1
  93. package/lib/typescript/src/pantallas/PantallaDePerfil.d.ts.map +1 -1
  94. package/package.json +1 -1
  95. package/src/camara/CampoDeFotos.tsx +4 -2
  96. package/src/camara/elegir.ts +4 -3
  97. package/src/componentes/AccionesDelEncabezado.tsx +3 -1
  98. package/src/componentes/BotonDeAvatar.tsx +22 -18
  99. package/src/componentes/BotonFlotante.tsx +65 -0
  100. package/src/componentes/CabeceraEmpujada.tsx +3 -1
  101. package/src/componentes/PantallaDeError.tsx +6 -4
  102. package/src/componentes/TiraDeFotos.tsx +4 -2
  103. package/src/componentes/VisorDeFoto.tsx +7 -7
  104. package/src/dialogos/HostDeDialogos.tsx +3 -1
  105. package/src/dialogos/dialogos.ts +15 -4
  106. package/src/formularios/SelectorDeOpcion.tsx +22 -8
  107. package/src/formularios/campos.tsx +9 -4
  108. package/src/formularios/guardado.ts +4 -2
  109. package/src/fotos/elegir.ts +14 -10
  110. package/src/hoja/HojaDeLista.tsx +5 -5
  111. package/src/hoja/useMasDeLaFicha.tsx +3 -1
  112. package/src/idioma/ProveedorDeIdioma.tsx +51 -0
  113. package/src/idioma/index.ts +7 -0
  114. package/src/idioma/textos.ts +246 -0
  115. package/src/index.ts +12 -0
  116. package/src/listas/HojaDeFiltros.tsx +10 -9
  117. package/src/listas/ListaPaginada.tsx +6 -6
  118. package/src/listas/piezas.tsx +17 -10
  119. package/src/pantallas/PantallaDeAcceso.tsx +6 -4
  120. package/src/pantallas/PantallaDeEdicion.tsx +13 -6
  121. package/src/pantallas/PantallaDeFicha.tsx +7 -5
  122. package/src/pantallas/PantallaDeHistorial.tsx +22 -16
  123. package/src/pantallas/PantallaDePerfil.tsx +12 -10
@@ -15,6 +15,7 @@ import IconoDeRenglon from "../componentes/IconoDeRenglon";
15
15
  import { crearEstilos, renglon, tipografia, usePaleta } from "../tema";
16
16
  import { Text, TextInput } from "../texto";
17
17
  import { CajaDeCampo } from "./campos";
18
+ import { useTextos } from "../idioma";
18
19
 
19
20
  /** Una opción de una lista: lo que se guarda (`value`) y lo que se lee (`label`). */
20
21
  export interface Opcion {
@@ -111,7 +112,7 @@ function HojaDeOpciones<T extends Opcion>({
111
112
  onTocar,
112
113
  detalle,
113
114
  grupo,
114
- vacio = "Nada que coincida.",
115
+ vacio,
115
116
  encabezado,
116
117
  pie,
117
118
  }: {
@@ -133,12 +134,13 @@ function HojaDeOpciones<T extends Opcion>({
133
134
  const c = usePaleta();
134
135
  const styles = useStyles();
135
136
  const insets = useSafeAreaInsets();
137
+ const t = useTextos();
136
138
 
137
139
  return (
138
140
  <Modal visible={abierto} animationType="slide" onRequestClose={onCerrar}>
139
141
  <View style={[styles.hoja, { paddingTop: insets.top + 10 }]}>
140
142
  <View style={styles.cabecera}>
141
- <TouchableOpacity onPress={onCerrar}>
143
+ <TouchableOpacity onPress={onCerrar} accessibilityLabel={t.comun.atras}>
142
144
  <MaterialIcons name="arrow-back" size={45} color={c.primary} />
143
145
  </TouchableOpacity>
144
146
  <Text style={styles.titulo}>{etiqueta}</Text>
@@ -146,7 +148,7 @@ function HojaDeOpciones<T extends Opcion>({
146
148
  <TextInput
147
149
  value={texto}
148
150
  onChangeText={onBuscar}
149
- placeholder="Buscar"
151
+ placeholder={t.comun.buscar}
150
152
  placeholderTextColor={c.secondaryLight}
151
153
  autoCorrect={false}
152
154
  autoCapitalize="characters"
@@ -163,7 +165,12 @@ function HojaDeOpciones<T extends Opcion>({
163
165
  }}
164
166
  ListHeaderComponent={encabezado}
165
167
  ListEmptyComponent={
166
- cargando ? null : <EstadoVacio icono="magnify" titulo={vacio} />
168
+ cargando ? null : (
169
+ <EstadoVacio
170
+ icono="magnify"
171
+ titulo={vacio ?? t.listas.nadaQueCoincida}
172
+ />
173
+ )
167
174
  }
168
175
  renderItem={({ item, index }) => {
169
176
  const rotulo = grupo?.(item);
@@ -217,6 +224,7 @@ function RenglonDelSelector({
217
224
  }) {
218
225
  const c = usePaleta();
219
226
  const styles = useStyles();
227
+ const t = useTextos();
220
228
  return (
221
229
  <CajaDeCampo
222
230
  etiqueta={etiqueta}
@@ -227,7 +235,9 @@ function RenglonDelSelector({
227
235
  pendiente={pendiente}
228
236
  >
229
237
  <View style={styles.renglon}>
230
- <Text style={[styles.valor, !texto && styles.sinValor]}>{texto ?? "Elegir…"}</Text>
238
+ <Text style={[styles.valor, !texto && styles.sinValor]}>
239
+ {texto ?? t.comun.elegir}
240
+ </Text>
231
241
  <IconoDeRenglon
232
242
  name="chevron-down"
233
243
  fontSize={tipografia.subtitulo}
@@ -253,13 +263,14 @@ export default function SelectorDeOpcion<T extends Opcion>(props: PropsDeSelecto
253
263
  detalle,
254
264
  grupo,
255
265
  opcional,
256
- textoDeNinguno = "Ninguno",
266
+ textoDeNinguno,
257
267
  vacio,
258
268
  disabled,
259
269
  accesorio,
260
270
  resaltarVacio,
261
271
  } = props;
262
272
  const styles = useStyles();
273
+ const t = useTextos();
263
274
  const [abierto, setAbierto] = React.useState(false);
264
275
  const { texto, buscar, visibles } = useBuscador(props);
265
276
 
@@ -296,7 +307,9 @@ export default function SelectorDeOpcion<T extends Opcion>(props: PropsDeSelecto
296
307
  encabezado={
297
308
  opcional ? (
298
309
  <Pressable onPress={() => elegir(null)} style={styles.opcion}>
299
- <Text style={styles.opcionNombre}>{textoDeNinguno}</Text>
310
+ <Text style={styles.opcionNombre}>
311
+ {textoDeNinguno ?? t.comun.ninguno}
312
+ </Text>
300
313
  </Pressable>
301
314
  ) : null
302
315
  }
@@ -329,6 +342,7 @@ export function SelectorDeVarias<T extends Opcion>(props: PropsDeSelectorDeVaria
329
342
  } = props;
330
343
  const [abierto, setAbierto] = React.useState(false);
331
344
  const [marcadas, setMarcadas] = React.useState<T[]>(valor);
345
+ const t = useTextos();
332
346
  const { texto, buscar, visibles } = useBuscador(props);
333
347
 
334
348
  const abrir = () => {
@@ -377,7 +391,7 @@ export function SelectorDeVarias<T extends Opcion>(props: PropsDeSelectorDeVaria
377
391
  cerrar();
378
392
  }}
379
393
  >
380
- {marcadas.length ? `Listo (${marcadas.length})` : "Listo"}
394
+ {marcadas.length ? t.listas.listoCon(marcadas.length) : t.comun.listo}
381
395
  </BotonGrande>
382
396
  }
383
397
  />
@@ -12,6 +12,7 @@ import IconoDeRenglon from "../componentes/IconoDeRenglon";
12
12
  import OpcionesExcluyentes from "../componentes/OpcionesExcluyentes";
13
13
  import { crearEstilos, renglon, tipografia, usePaleta } from "../tema";
14
14
  import { Text, TextInput } from "../texto";
15
+ import { useTextos } from "../idioma";
15
16
 
16
17
  /**
17
18
  * Los campos de un formulario (un viaje, un tracto, un operador). Cada uno es una caja con su
@@ -245,6 +246,7 @@ export function CampoDeFechaHora({
245
246
  const c = usePaleta();
246
247
  const styles = useStyles();
247
248
  const [eligiendo, setEligiendo] = React.useState<"date" | "time" | null>(null);
249
+ const t = useTextos();
248
250
  // Vacío, el selector arranca en la próxima hora en punto: la hora de ahora no le sirve a nadie.
249
251
  const base = valor ?? dayjs().add(1, "hour").startOf("hour").toDate();
250
252
 
@@ -296,7 +298,7 @@ export function CampoDeFechaHora({
296
298
  fontSize={tipografia.subtitulo}
297
299
  color={c.secondaryLight}
298
300
  />
299
- <Text style={[styles.textoFecha, styles.sinValor]}>Elegir…</Text>
301
+ <Text style={[styles.textoFecha, styles.sinValor]}>{t.comun.elegir}</Text>
300
302
  </View>
301
303
  )}
302
304
  </CajaDeCampo>
@@ -335,12 +337,13 @@ export function CampoDeFechaHora({
335
337
  /** El «Quitar» de una fecha opcional: la ✕ junto al rótulo, dentro de la caja. */
336
338
  function BotonDeQuitar({ onPress }: { onPress: () => void }) {
337
339
  const c = usePaleta();
340
+ const t = useTextos();
338
341
  return (
339
342
  <Pressable
340
343
  onPress={onPress}
341
344
  hitSlop={10}
342
345
  accessibilityRole="button"
343
- accessibilityLabel="Quitar"
346
+ accessibilityLabel={t.comun.quitar}
344
347
  >
345
348
  <IconoDeRenglon
346
349
  name="close-circle"
@@ -418,6 +421,7 @@ export function CampoDeFecha({
418
421
  const c = usePaleta();
419
422
  const styles = useStyles();
420
423
  const [eligiendo, setEligiendo] = React.useState(false);
424
+ const t = useTextos();
421
425
  const fecha = valor ? dayjs(valor) : null;
422
426
 
423
427
  return (
@@ -438,7 +442,7 @@ export function CampoDeFecha({
438
442
  color={fecha ? c.secondary : c.secondaryLight}
439
443
  />
440
444
  <Text style={[styles.textoFecha, !fecha && styles.sinValor]}>
441
- {fecha ? fecha.format("D MMM YYYY") : "Elegir…"}
445
+ {fecha ? fecha.format("D MMM YYYY") : t.comun.elegir}
442
446
  </Text>
443
447
  </View>
444
448
  </CajaDeCampo>
@@ -528,6 +532,7 @@ export function CampoDeTelefono({
528
532
  const styles = useStyles();
529
533
  const { entrada, enfocada, enfocar, alEnfocar, alSalir } = useEntradaEnCaja();
530
534
  const { lada, digitos } = partirTelefono(valor);
535
+ const t = useTextos();
531
536
  const armar = (nuevaLada: string, nuevosDigitos: string) => {
532
537
  const solo = nuevosDigitos.replace(/\D/g, "");
533
538
  onCambiar(solo ? `${nuevaLada}${solo}` : null);
@@ -569,7 +574,7 @@ export function CampoDeTelefono({
569
574
  onBlur={alSalir}
570
575
  keyboardType="phone-pad"
571
576
  maxLength={10}
572
- placeholder="10 dígitos"
577
+ placeholder={t.campos.diezDigitos}
573
578
  accessibilityLabel={etiqueta}
574
579
  style={styles.texto}
575
580
  />
@@ -3,6 +3,7 @@ import React from "react";
3
3
  import type { FieldValues, UseFormReturn } from "react-hook-form";
4
4
  import { avisar } from "../dialogos/dialogos";
5
5
  import { mensajeDeError, setErrors } from "./errores";
6
+ import { textosActuales } from "../idioma";
6
7
 
7
8
  /**
8
9
  * Lo que comparten los formularios de edición: cómo se compara lo capturado contra lo que vino,
@@ -54,9 +55,10 @@ export function useGuardado<T extends FieldValues>(form: UseFormReturn<T>) {
54
55
  router.back();
55
56
  } catch (falla) {
56
57
  volcarErrores(form, falla);
57
- const mensaje = mensajeDeError(falla, "Revisa la señal e intenta de nuevo.");
58
+ const t = textosActuales();
59
+ const mensaje = mensajeDeError(falla, t.comun.revisaLaSenal);
58
60
  setError(mensaje);
59
- await avisar({ titulo: "No se guardó", mensaje, icono: "alert-circle-outline" });
61
+ await avisar({ titulo: t.edicion.noSeGuardo, mensaje, icono: "alert-circle-outline" });
60
62
  }
61
63
  };
62
64
  return { error, guardar };
@@ -11,6 +11,7 @@ import {
11
11
  } from "../camara/elegir";
12
12
  import { reducirImagenAdjunta } from "../camara/reducir";
13
13
  import { avisar, mostrarDialogo } from "../dialogos/dialogos";
14
+ import { textosActuales } from "../idioma";
14
15
 
15
16
  /** El tope de un archivo en el servidor (`ConArchivoSubido.TAMANO_MAXIMO`): 25 MB. */
16
17
  export const TAMANO_MAXIMO_BYTES = 25 * 1024 * 1024;
@@ -48,10 +49,12 @@ export async function elegirDocumentos({
48
49
  const pesa = doc.size ?? new File(doc.uri).size;
49
50
  if (pesa > tamanoMaximo) {
50
51
  await avisar({
51
- titulo: "Archivo demasiado pesado",
52
- mensaje:
53
- `«${doc.name}» pesa ${(pesa / (1024 * 1024)).toFixed(1)} MB y el tope son ` +
54
- `${tamanoMaximo / (1024 * 1024)} MB. Elige uno más ligero o tómale una foto.`,
52
+ titulo: textosActuales().fotos.demasiadoPesado,
53
+ mensaje: textosActuales().fotos.pesaDeMas(
54
+ doc.name,
55
+ (pesa / (1024 * 1024)).toFixed(1),
56
+ tamanoMaximo / (1024 * 1024),
57
+ ),
55
58
  icono: "file-alert-outline",
56
59
  });
57
60
  continue;
@@ -71,7 +74,7 @@ export async function elegirDocumentos({
71
74
  * cualquier paso o el archivo no se puede subir (y ya se dijo por qué).
72
75
  */
73
76
  export async function elegirAdjunto({
74
- titulo = "Subir un archivo",
77
+ titulo,
75
78
  permiso,
76
79
  tipos,
77
80
  }: {
@@ -79,29 +82,30 @@ export async function elegirAdjunto({
79
82
  permiso: PermisoDeCamara;
80
83
  tipos?: string[];
81
84
  }): Promise<ArchivoElegido | null> {
85
+ const t = textosActuales();
82
86
  const origen = await mostrarDialogo<"camara" | "galeria" | "documento" | null>({
83
- titulo,
87
+ titulo: titulo ?? t.fotos.subirArchivo,
84
88
  icono: "paperclip",
85
89
  botones: [
86
90
  {
87
- texto: "Tomar una foto",
91
+ texto: t.fotos.tomarUnaFoto,
88
92
  icono: "camera-outline",
89
93
  estilo: "principal",
90
94
  valor: "camara",
91
95
  },
92
96
  {
93
- texto: "De la galería",
97
+ texto: t.fotos.deLaGaleria,
94
98
  icono: "image-outline",
95
99
  estilo: "secundario",
96
100
  valor: "galeria",
97
101
  },
98
102
  {
99
- texto: "Un documento",
103
+ texto: t.fotos.unDocumento,
100
104
  icono: "file-document-outline",
101
105
  estilo: "secundario",
102
106
  valor: "documento",
103
107
  },
104
- { texto: "Cancelar", estilo: "cancelar", valor: null },
108
+ { texto: t.comun.cancelar, estilo: "cancelar", valor: null },
105
109
  ],
106
110
  });
107
111
  const elegidos =
@@ -13,6 +13,7 @@ import { EnHoja } from "../componentes/Tarjeta";
13
13
  import { Buscador } from "../listas/piezas";
14
14
  import { crearEstilos, renglon, tipografia, usePaleta } from "../tema";
15
15
  import { Text } from "../texto";
16
+ import { useTextos } from "../idioma";
16
17
 
17
18
  /** Lo alto de la hoja: arriba se sigue viendo un poco de la ficha, que dice dónde se está. */
18
19
  const ALTO = "85%";
@@ -76,6 +77,7 @@ export default function HojaDeLista<T>({
76
77
  }) {
77
78
  const c = usePaleta();
78
79
  const styles = useStyles();
80
+ const t = useTextos();
79
81
  const hoja = useRef<BottomSheet>(null);
80
82
  const insets = useSafeAreaInsets();
81
83
 
@@ -123,9 +125,7 @@ export default function HojaDeLista<T>({
123
125
  ) : null}
124
126
  {acciones}
125
127
  {nombres && total != null && renglones.length ? (
126
- <Text style={styles.cuenta}>
127
- {total === 1 ? `1 ${nombres[0]}` : `${total} ${nombres[1]}`}
128
- </Text>
128
+ <Text style={styles.cuenta}>{t.listas.cuantos(total, nombres)}</Text>
129
129
  ) : null}
130
130
  </View>
131
131
  );
@@ -134,9 +134,9 @@ export default function HojaDeLista<T>({
134
134
  <ActivityIndicator color={c.primary} style={styles.espera} />
135
135
  ) : lista.fallo ? (
136
136
  <View style={styles.vacio}>
137
- <Text style={styles.linea}>No se pudo cargar. Revisa tu señal.</Text>
137
+ <Text style={styles.linea}>{t.ficha.hojaNoCargo}</Text>
138
138
  <BotonGrande icono="refresh" modo="contorno" onPress={lista.onReintentar}>
139
- Reintentar
139
+ {t.comun.reintentar}
140
140
  </BotonGrande>
141
141
  </View>
142
142
  ) : (
@@ -1,6 +1,7 @@
1
1
  import HojaInferior from "./HojaInferior";
2
2
  import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
3
3
  import React from "react";
4
+ import { useTextos } from "../idioma";
4
5
  import { FilaConContador, SeccionDeFicha } from "../listas/piezas";
5
6
 
6
7
  /** Una lista de «Más»: su renglón con contador y lo que abre en la hoja. */
@@ -26,13 +27,14 @@ export interface ListaDeLaFicha {
26
27
  */
27
28
  export function useMasDeLaFicha(listas: ListaDeLaFicha[], subtitulo?: string) {
28
29
  // Lo que enseña se queda al cerrarla, para que su título no se vacíe mientras baja.
30
+ const t = useTextos();
29
31
  const [clave, setClave] = React.useState<string | null>(null);
30
32
  const [abierta, setAbierta] = React.useState(false);
31
33
  const cerrar = React.useCallback(() => setAbierta(false), []);
32
34
  const elegida = listas.find((l) => l.clave === clave);
33
35
 
34
36
  const seccion = listas.length ? (
35
- <SeccionDeFicha titulo="Más">
37
+ <SeccionDeFicha titulo={t.ficha.mas}>
36
38
  {listas.map((l) => (
37
39
  <FilaConContador
38
40
  key={l.clave}
@@ -0,0 +1,51 @@
1
+ import React, { createContext, useContext } from "react";
2
+ import TEXTOS, { type Idioma, type Textos } from "./textos";
3
+
4
+ // ==============================|| IDIOMA ||============================== //
5
+
6
+ /**
7
+ * El idioma en el que la librería dibuja lo suyo. La librería **no traduce**: recibe el idioma ya
8
+ * resuelto y elige diccionario. De dónde sale (el teléfono, un claim del token, lo que el usuario
9
+ * eligió) y dónde se cambia es asunto de la app. Es el mismo trato que `LocaleProvider` en
10
+ * `@acvl/frontend-components`.
11
+ *
12
+ * Sin proveedor el idioma es español, que es como se comportaba antes de que esto existiera: una
13
+ * app que no lo monta no cambia en nada.
14
+ *
15
+ * @example
16
+ * <ProveedorDeIdioma idioma={idioma}>
17
+ * <Stack />
18
+ * </ProveedorDeIdioma>
19
+ */
20
+ const ContextoDeIdioma = createContext<Idioma>("es");
21
+
22
+ /**
23
+ * El idioma vigente para lo que corre fuera de un componente (`avisar`, `confirmar`,
24
+ * `elegirAdjunto`): no pueden leer un contexto. Lo pone el proveedor al pintarse.
25
+ */
26
+ let idiomaVigente: Idioma = "es";
27
+
28
+ export function ProveedorDeIdioma({
29
+ idioma,
30
+ children,
31
+ }: {
32
+ idioma: Idioma;
33
+ children: React.ReactNode;
34
+ }) {
35
+ // En el render y no en un efecto: un `avisar` que salga del primer render de los hijos ya
36
+ // tiene que hablar el idioma nuevo. Es idempotente, así que StrictMode no lo descompone.
37
+ idiomaVigente = idioma;
38
+ return <ContextoDeIdioma.Provider value={idioma}>{children}</ContextoDeIdioma.Provider>;
39
+ }
40
+
41
+ /** El idioma activo de la librería. */
42
+ export const useIdiomaDeLaLibreria = (): Idioma => useContext(ContextoDeIdioma);
43
+
44
+ /**
45
+ * Los textos de la librería en el idioma activo. Devuelve el diccionario entero y no una función:
46
+ * `t.comun.cerrar` se autocompleta y una llave mal escrita no compila.
47
+ */
48
+ export const useTextos = (): Textos => TEXTOS[useContext(ContextoDeIdioma)] ?? TEXTOS.es;
49
+
50
+ /** Los textos fuera de un componente; dentro de uno, `useTextos`. */
51
+ export const textosActuales = (): Textos => TEXTOS[idiomaVigente] ?? TEXTOS.es;
@@ -0,0 +1,7 @@
1
+ export {
2
+ ProveedorDeIdioma,
3
+ textosActuales,
4
+ useIdiomaDeLaLibreria,
5
+ useTextos,
6
+ } from "./ProveedorDeIdioma";
7
+ export { type Idioma, type Textos } from "./textos";
@@ -0,0 +1,246 @@
1
+ // ==============================|| TEXTOS DE LA LIBRERÍA ||============================== //
2
+
3
+ /**
4
+ * Los textos que la librería dibuja por su cuenta, en los dos idiomas. Es el espejo del de
5
+ * `@acvl/frontend-components`: un diccionario tipado y no un runtime de i18n, para que cada app
6
+ * traiga el suyo (o ninguno) sin que la librería le imponga `i18next`. A `en` le falta una llave y
7
+ * no compila.
8
+ *
9
+ * El idioma lo pone la app con `ProveedorDeIdioma`; se lee con `useTextos()` en un componente y con
10
+ * `textosActuales()` fuera de uno (`avisar`, `elegirAdjunto`).
11
+ *
12
+ * Las plantillas van como funciones: el orden de las palabras cambia entre idiomas y una
13
+ * concatenación en el punto de uso lo fija.
14
+ */
15
+
16
+ /** Idiomas que entiende la librería. */
17
+ export type Idioma = "es" | "en";
18
+
19
+ const es = {
20
+ comun: {
21
+ cancelar: "Cancelar",
22
+ cerrar: "Cerrar",
23
+ entendido: "Entendido",
24
+ reintentar: "Reintentar",
25
+ listo: "Listo",
26
+ quitar: "Quitar",
27
+ elegir: "Elegir…",
28
+ buscar: "Buscar",
29
+ ninguno: "Ninguno",
30
+ noDisponible: "N/A",
31
+ si: "Sí",
32
+ no: "No",
33
+ atras: "Atrás",
34
+ revisaLaSenal: "Revisa tu señal e intenta de nuevo.",
35
+ noSePudoCargar: "No se pudo cargar",
36
+ },
37
+ acceso: {
38
+ usuario: "Usuario",
39
+ contrasena: "Contraseña",
40
+ entrar: "Iniciar Sesión",
41
+ noSePudo: "No se pudo iniciar sesión. Intente de nuevo.",
42
+ },
43
+ perfil: {
44
+ titulo: "Perfil",
45
+ apariencia: "Apariencia",
46
+ telefono: "Teléfono",
47
+ claro: "Claro",
48
+ oscuro: "Oscuro",
49
+ cerrarSesion: "Cerrar sesión",
50
+ },
51
+ error: {
52
+ titulo: "Algo salió mal",
53
+ mensaje:
54
+ "Esta pantalla no se pudo abrir. Intenta de nuevo; si vuelve a pasar, avisa a soporte.",
55
+ },
56
+ ficha: {
57
+ noExiste: "No existe",
58
+ noExisteTexto: "Puede que lo hayan borrado, o que no tengas permiso de verlo.",
59
+ mas: "Más",
60
+ hojaNoCargo: "No se pudo cargar. Revisa tu señal.",
61
+ seccionNoCargo: "No se pudo cargar. Jala la ficha para releer.",
62
+ },
63
+ edicion: {
64
+ sinPermiso: "No tienes permiso",
65
+ noSePudoCargar: (queEs: string) => `No se pudo cargar ${queEs}`,
66
+ guardar: "Guardar",
67
+ guardando: "Guardando…",
68
+ noSeGuardo: "No se guardó",
69
+ },
70
+ listas: {
71
+ cuantos: (total: number, [uno, varios]: readonly [string, string]) =>
72
+ total === 1 ? `1 ${uno}` : `${total} ${varios}`,
73
+ borrarBusqueda: "Borrar búsqueda",
74
+ filtrosPuestos: (cuenta: number) => `Filtros, ${cuenta} puestos`,
75
+ filtrosYOrden: "Filtros y orden",
76
+ filtros: "Filtros",
77
+ ver: (total: number, [uno, varios]: readonly [string, string]) =>
78
+ `Ver ${total} ${total === 1 ? uno : varios}`,
79
+ ordenarPor: "Ordenar por",
80
+ sentido: "Sentido",
81
+ ascendente: "Ascendente",
82
+ descendente: "Descendente",
83
+ nadaQueCoincida: "Nada que coincida.",
84
+ listoCon: (n: number) => `Listo (${n})`,
85
+ masAcciones: "Más acciones",
86
+ },
87
+ contacto: {
88
+ llamar: (nombre: string) => `Llamar a ${nombre}`,
89
+ whatsapp: (nombre: string) => `WhatsApp a ${nombre}`,
90
+ correo: (nombre: string) => `Correo a ${nombre}`,
91
+ direccion: "Dirección",
92
+ abrirEnElMapa: (donde: string) => `Abrir en el mapa: ${donde}`,
93
+ },
94
+ campos: {
95
+ diezDigitos: "10 dígitos",
96
+ },
97
+ fotos: {
98
+ verLaFoto: "Ver la foto",
99
+ archivo: (nombre: string) => `Archivo ${nombre}`.trim(),
100
+ quitarFoto: "Quitar esta foto",
101
+ quitarArchivo: "Quitar este archivo",
102
+ guardada: "Guardada en la galería",
103
+ noSeGuardo: "No se pudo guardar; intentar de nuevo",
104
+ guardarEnGaleria: "Guardar en la galería",
105
+ deTotal: (n: number, total: number) => `${n} de ${total}`,
106
+ tomarFoto: "Tomar foto",
107
+ tomarUnaFoto: "Tomar una foto",
108
+ deLaGaleria: "De la galería",
109
+ unDocumento: "Un documento",
110
+ subirArchivo: "Subir un archivo",
111
+ sinPermisoDeCamara: "Sin permiso de cámara",
112
+ activaLaCamara: (para: string) =>
113
+ `Para ${para}, activa la cámara para la app en los ajustes del teléfono.`,
114
+ laCamara: "La cámara",
115
+ demasiadoPesado: "Archivo demasiado pesado",
116
+ pesaDeMas: (nombre: string, pesaMb: string, topeMb: number) =>
117
+ `«${nombre}» pesa ${pesaMb} MB y el tope son ${topeMb} MB. Elige uno más ligero o tómale una foto.`,
118
+ },
119
+ historial: {
120
+ titulo: "Historial",
121
+ sistema: "Sistema",
122
+ sinCambios: "Sin cambios en los campos.",
123
+ ocultarCampos: "Ocultar campos",
124
+ verTodos: "Ver todos los campos",
125
+ verCampos: "Ver los campos",
126
+ noCargo: "No se pudo cargar el historial",
127
+ sinMovimientos: "Sin movimientos registrados",
128
+ },
129
+ };
130
+
131
+ export type Textos = typeof es;
132
+
133
+ const en: Textos = {
134
+ comun: {
135
+ cancelar: "Cancel",
136
+ cerrar: "Close",
137
+ entendido: "Got it",
138
+ reintentar: "Try again",
139
+ listo: "Done",
140
+ quitar: "Clear",
141
+ elegir: "Choose…",
142
+ buscar: "Search",
143
+ ninguno: "None",
144
+ noDisponible: "N/A",
145
+ si: "Yes",
146
+ no: "No",
147
+ atras: "Back",
148
+ revisaLaSenal: "Check your connection and try again.",
149
+ noSePudoCargar: "Couldn't load",
150
+ },
151
+ acceso: {
152
+ usuario: "Username",
153
+ contrasena: "Password",
154
+ entrar: "Sign in",
155
+ noSePudo: "Couldn't sign in. Please try again.",
156
+ },
157
+ perfil: {
158
+ titulo: "Profile",
159
+ apariencia: "Appearance",
160
+ telefono: "Phone",
161
+ claro: "Light",
162
+ oscuro: "Dark",
163
+ cerrarSesion: "Sign out",
164
+ },
165
+ error: {
166
+ titulo: "Something went wrong",
167
+ mensaje: "This screen couldn't open. Try again; if it keeps happening, contact support.",
168
+ },
169
+ ficha: {
170
+ noExiste: "Not found",
171
+ noExisteTexto: "It may have been deleted, or you may not have permission to see it.",
172
+ mas: "More",
173
+ hojaNoCargo: "Couldn't load. Check your connection.",
174
+ seccionNoCargo: "Couldn't load. Pull down to refresh.",
175
+ },
176
+ edicion: {
177
+ sinPermiso: "You don't have permission",
178
+ noSePudoCargar: (queEs: string) => `Couldn't load ${queEs}`,
179
+ guardar: "Save",
180
+ guardando: "Saving…",
181
+ noSeGuardo: "Not saved",
182
+ },
183
+ listas: {
184
+ cuantos: (total: number, [uno, varios]: readonly [string, string]) =>
185
+ total === 1 ? `1 ${uno}` : `${total} ${varios}`,
186
+ borrarBusqueda: "Clear search",
187
+ filtrosPuestos: (cuenta: number) => `Filters, ${cuenta} applied`,
188
+ filtrosYOrden: "Filters and sorting",
189
+ filtros: "Filters",
190
+ ver: (total: number, [uno, varios]: readonly [string, string]) =>
191
+ `Show ${total} ${total === 1 ? uno : varios}`,
192
+ ordenarPor: "Sort by",
193
+ sentido: "Direction",
194
+ ascendente: "Ascending",
195
+ descendente: "Descending",
196
+ nadaQueCoincida: "No matches.",
197
+ listoCon: (n: number) => `Done (${n})`,
198
+ masAcciones: "More actions",
199
+ },
200
+ contacto: {
201
+ llamar: (nombre: string) => `Call ${nombre}`,
202
+ whatsapp: (nombre: string) => `WhatsApp ${nombre}`,
203
+ correo: (nombre: string) => `Email ${nombre}`,
204
+ direccion: "Address",
205
+ abrirEnElMapa: (donde: string) => `Open in maps: ${donde}`,
206
+ },
207
+ campos: {
208
+ diezDigitos: "10 digits",
209
+ },
210
+ fotos: {
211
+ verLaFoto: "View photo",
212
+ archivo: (nombre: string) => `File ${nombre}`.trim(),
213
+ quitarFoto: "Remove this photo",
214
+ quitarArchivo: "Remove this file",
215
+ guardada: "Saved to gallery",
216
+ noSeGuardo: "Couldn't save; try again",
217
+ guardarEnGaleria: "Save to gallery",
218
+ deTotal: (n: number, total: number) => `${n} of ${total}`,
219
+ tomarFoto: "Take photo",
220
+ tomarUnaFoto: "Take a photo",
221
+ deLaGaleria: "From gallery",
222
+ unDocumento: "A document",
223
+ subirArchivo: "Upload a file",
224
+ sinPermisoDeCamara: "No camera permission",
225
+ activaLaCamara: (para: string) =>
226
+ `To ${para}, turn on camera access for the app in your phone's settings.`,
227
+ laCamara: "The camera",
228
+ demasiadoPesado: "File too large",
229
+ pesaDeMas: (nombre: string, pesaMb: string, topeMb: number) =>
230
+ `"${nombre}" is ${pesaMb} MB and the limit is ${topeMb} MB. Choose a smaller one or take a photo of it.`,
231
+ },
232
+ historial: {
233
+ titulo: "History",
234
+ sistema: "System",
235
+ sinCambios: "No field changes.",
236
+ ocultarCampos: "Hide fields",
237
+ verTodos: "Show all fields",
238
+ verCampos: "Show fields",
239
+ noCargo: "Couldn't load the history",
240
+ sinMovimientos: "No recorded activity",
241
+ },
242
+ };
243
+
244
+ const TEXTOS: Record<Idioma, Textos> = { es, en };
245
+
246
+ export default TEXTOS;
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export {
10
10
  type AccionDelEncabezado,
11
11
  } from "./componentes/AccionesDelEncabezado";
12
12
  export { default as BotonDeAvatar, inicialesDe } from "./componentes/BotonDeAvatar";
13
+ export { default as BotonFlotante, HUECO_DEL_BOTON_FLOTANTE } from "./componentes/BotonFlotante";
13
14
  export { default as BotonGrande } from "./componentes/BotonGrande";
14
15
  export { default as CabeceraEmpujada } from "./componentes/CabeceraEmpujada";
15
16
  export { default as Chip, type TonoDeChip } from "./componentes/Chip";
@@ -150,3 +151,14 @@ export {
150
151
  type Historial,
151
152
  } from "./pantallas/PantallaDeHistorial";
152
153
  export { default as PantallaDePerfil } from "./pantallas/PantallaDePerfil";
154
+
155
+ // ==============================|| IDIOMA ||============================== //
156
+
157
+ export {
158
+ ProveedorDeIdioma,
159
+ textosActuales,
160
+ useIdiomaDeLaLibreria,
161
+ useTextos,
162
+ type Idioma,
163
+ type Textos,
164
+ } from "./idioma";