@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
@@ -6,6 +6,7 @@ import MaterialIcons from "@expo/vector-icons/MaterialIcons";
6
6
  import React from "react";
7
7
  import { Modal, Pressable, ScrollView, Switch, TouchableOpacity, View } from "react-native";
8
8
  import { useSafeAreaInsets } from "react-native-safe-area-context";
9
+ import { useTextos } from "../idioma";
9
10
 
10
11
  /**
11
12
  * La hoja de filtros y orden de una lista (viajes, recursos), a pantalla completa
@@ -35,17 +36,18 @@ export default function HojaDeFiltros({
35
36
  const c = usePaleta();
36
37
  const styles = useStyles();
37
38
  const insets = useSafeAreaInsets();
39
+ const t = useTextos();
38
40
  return (
39
41
  <Modal visible={abierta} animationType="slide" onRequestClose={onCerrar}>
40
42
  <View style={[styles.hoja, { paddingTop: insets.top + 10 }]}>
41
43
  <View style={styles.cabecera}>
42
- <TouchableOpacity onPress={onCerrar} accessibilityLabel="Cerrar">
44
+ <TouchableOpacity onPress={onCerrar} accessibilityLabel={t.comun.cerrar}>
43
45
  <MaterialIcons name="arrow-back" size={45} color={c.primary} />
44
46
  </TouchableOpacity>
45
- <Text style={styles.titulo}>Filtros</Text>
47
+ <Text style={styles.titulo}>{t.listas.filtros}</Text>
46
48
  {hayAlgo ? (
47
49
  <TouchableOpacity onPress={onQuitar}>
48
- <Text style={styles.limpiar}>Quitar</Text>
50
+ <Text style={styles.limpiar}>{t.comun.quitar}</Text>
49
51
  </TouchableOpacity>
50
52
  ) : null}
51
53
  </View>
@@ -54,9 +56,7 @@ export default function HojaDeFiltros({
54
56
 
55
57
  <View style={{ paddingBottom: insets.bottom + 16 }}>
56
58
  <BotonGrande onPress={onCerrar}>
57
- {visibles === null
58
- ? "Listo"
59
- : `Ver ${visibles} ${visibles === 1 ? nombres[0] : nombres[1]}`}
59
+ {visibles === null ? t.comun.listo : t.listas.ver(visibles, nombres)}
60
60
  </BotonGrande>
61
61
  </View>
62
62
  </View>
@@ -205,17 +205,18 @@ export function OrdenDeLista({
205
205
  onOrdenar: (orden: string | null) => void;
206
206
  porOmision?: string;
207
207
  }) {
208
+ const t = useTextos();
208
209
  const descendente = orden?.startsWith("-") ?? false;
209
210
  const campo = orden?.replace(/^-/, "");
210
211
  const elegido = campos.find((c) => c.value === campo);
211
- const sentidos = elegido?.sentidos ?? { asc: "Ascendente", desc: "Descendente" };
212
+ const sentidos = elegido?.sentidos ?? { asc: t.listas.ascendente, desc: t.listas.descendente };
212
213
  // El sentido con el que arranca va primero: en una fecha se lee «Recientes primero» antes
213
214
  // que «Antiguos primero».
214
215
  const ordenDeSentidos: ("asc" | "desc")[] =
215
216
  elegido?.primero === "desc" ? ["desc", "asc"] : ["asc", "desc"];
216
217
  return (
217
218
  <>
218
- <SeccionDeFiltros>Ordenar por</SeccionDeFiltros>
219
+ <SeccionDeFiltros>{t.listas.ordenarPor}</SeccionDeFiltros>
219
220
  {porOmision ? (
220
221
  <OpcionDeFiltro
221
222
  texto={porOmision}
@@ -240,7 +241,7 @@ export function OrdenDeLista({
240
241
  ))}
241
242
  {campo !== undefined ? (
242
243
  <>
243
- <SeccionDeFiltros>Sentido</SeccionDeFiltros>
244
+ <SeccionDeFiltros>{t.listas.sentido}</SeccionDeFiltros>
244
245
  {ordenDeSentidos.map((sentido) => (
245
246
  <OpcionDeFiltro
246
247
  key={sentido}
@@ -7,6 +7,7 @@ import type MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIco
7
7
  import React from "react";
8
8
  import { ActivityIndicator, FlatList, RefreshControl } from "react-native";
9
9
  import { renglonesDe, type Pagina } from "./paginado";
10
+ import { useTextos } from "../idioma";
10
11
 
11
12
  /** Lo que la lista lee de un `use…InfiniteQuery` de RTK Query. */
12
13
  export interface ConsultaPaginada<T> {
@@ -50,6 +51,7 @@ export default function ListaPaginada<T>({
50
51
  }: Props<T>) {
51
52
  const c = usePaleta();
52
53
  const styles = useStyles();
54
+ const t = useTextos();
53
55
  const renglones = renglonesDe(consulta.data);
54
56
  const total = consulta.data?.pages[0]?.count;
55
57
 
@@ -60,11 +62,11 @@ export default function ListaPaginada<T>({
60
62
  return (
61
63
  <EstadoVacio
62
64
  icono="cloud-off-outline"
63
- titulo="No se pudo cargar"
64
- texto="Revisa tu señal e intenta de nuevo."
65
+ titulo={t.comun.noSePudoCargar}
66
+ texto={t.comun.revisaLaSenal}
65
67
  >
66
68
  <BotonGrande icono="refresh" onPress={() => consulta.refetch()}>
67
- Reintentar
69
+ {t.comun.reintentar}
68
70
  </BotonGrande>
69
71
  </EstadoVacio>
70
72
  );
@@ -78,9 +80,7 @@ export default function ListaPaginada<T>({
78
80
  contentContainerStyle={[styles.contenido, !renglones.length && styles.vacia]}
79
81
  ListHeaderComponent={
80
82
  total !== undefined && renglones.length ? (
81
- <Text style={styles.total}>
82
- {total === 1 ? `1 ${nombres[0]}` : `${total} ${nombres[1]}`}
83
- </Text>
83
+ <Text style={styles.total}>{t.listas.cuantos(total, nombres)}</Text>
84
84
  ) : null
85
85
  }
86
86
  ListEmptyComponent={
@@ -20,6 +20,7 @@ import {
20
20
  type TextInputProps,
21
21
  type ViewStyle,
22
22
  } from "react-native";
23
+ import { useTextos } from "../idioma";
23
24
 
24
25
  /**
25
26
  * Las piezas de las listas y las fichas: la cabecera de una lista (buscador y pestañas), la
@@ -58,6 +59,7 @@ export function Buscador({
58
59
  }) {
59
60
  const c = usePaleta();
60
61
  const styles = useStyles();
62
+ const t = useTextos();
61
63
  const Campo = Entrada ?? TextInput;
62
64
  return (
63
65
  <View style={[styles.buscador, style]}>
@@ -90,7 +92,7 @@ export function Buscador({
90
92
  <Pressable
91
93
  onPress={() => onCambiar("")}
92
94
  hitSlop={8}
93
- accessibilityLabel="Borrar búsqueda"
95
+ accessibilityLabel={t.listas.borrarBusqueda}
94
96
  >
95
97
  <MaterialCommunityIcons name="close-circle" size={22} color={c.secondary} />
96
98
  </Pressable>
@@ -114,13 +116,14 @@ export interface FiltrosDeCabecera {
114
116
  */
115
117
  function BotonDeFiltros({ cuenta, punto, onPress }: FiltrosDeCabecera) {
116
118
  const c = usePaleta();
119
+ const t = useTextos();
117
120
  const styles = useStyles();
118
121
  const activo = Boolean(cuenta) || punto;
119
122
  return (
120
123
  <Pressable
121
124
  onPress={onPress}
122
125
  accessibilityRole="button"
123
- accessibilityLabel={cuenta ? `Filtros, ${cuenta} puestos` : "Filtros y orden"}
126
+ accessibilityLabel={cuenta ? t.listas.filtrosPuestos(cuenta) : t.listas.filtrosYOrden}
124
127
  hitSlop={4}
125
128
  style={({ pressed }) => [
126
129
  styles.botonDeFiltros,
@@ -361,6 +364,7 @@ export function RenglonDeContacto({
361
364
  }) {
362
365
  const c = usePaleta();
363
366
  const styles = useStyles();
367
+ const t = useTextos();
364
368
  const boton = (
365
369
  icono: React.ComponentProps<typeof MaterialCommunityIcons>["name"],
366
370
  etiqueta: string,
@@ -386,15 +390,17 @@ export function RenglonDeContacto({
386
390
  {correo ? <Text style={styles.linea}>{correo}</Text> : null}
387
391
  </View>
388
392
  <View style={styles.acciones}>
389
- {telefono ? boton("phone", `Llamar a ${nombre}`, `tel:${telefono}`) : null}
393
+ {telefono ? boton("phone", t.contacto.llamar(nombre), `tel:${telefono}`) : null}
390
394
  {telefono
391
395
  ? boton(
392
396
  "whatsapp",
393
- `WhatsApp a ${nombre}`,
397
+ t.contacto.whatsapp(nombre),
394
398
  `https://wa.me/${digitos(telefono)}`,
395
399
  )
396
400
  : null}
397
- {correo ? boton("email-outline", `Correo a ${nombre}`, `mailto:${correo}`) : null}
401
+ {correo
402
+ ? boton("email-outline", t.contacto.correo(nombre), `mailto:${correo}`)
403
+ : null}
398
404
  </View>
399
405
  </View>
400
406
  );
@@ -418,7 +424,7 @@ const abrirEnMapas = (direccion: string) => {
418
424
  * no hay renglón. Es el hermano de `RenglonDeContacto`.
419
425
  */
420
426
  export function RenglonDeDireccion({
421
- etiqueta = "Dirección",
427
+ etiqueta,
422
428
  lineas,
423
429
  enHoja,
424
430
  }: {
@@ -430,13 +436,14 @@ export function RenglonDeDireccion({
430
436
  }) {
431
437
  const c = usePaleta();
432
438
  const styles = useStyles();
439
+ const t = useTextos();
433
440
  const presentes = lineas.map((l) => l?.trim()).filter((l): l is string => Boolean(l));
434
441
  if (!presentes.length) return null;
435
442
  return (
436
443
  <Pressable
437
444
  onPress={() => abrirEnMapas(presentes.join(", "))}
438
445
  accessibilityRole="link"
439
- accessibilityLabel={`Abrir en el mapa: ${presentes.join(", ")}`}
446
+ accessibilityLabel={t.contacto.abrirEnElMapa(presentes.join(", "))}
440
447
  style={({ pressed }) => [
441
448
  styles.renglon,
442
449
  enHoja && styles.renglonEnHoja,
@@ -444,7 +451,7 @@ export function RenglonDeDireccion({
444
451
  ]}
445
452
  >
446
453
  <View style={styles.renglonTexto}>
447
- <Text style={styles.etiqueta}>{etiqueta}</Text>
454
+ <Text style={styles.etiqueta}>{etiqueta ?? t.contacto.direccion}</Text>
448
455
  {presentes.map((l, i) => (
449
456
  <Text key={i} style={i === 0 ? styles.valor : styles.linea}>
450
457
  {l}
@@ -544,9 +551,9 @@ export function EstadoDeHoja({
544
551
  }) {
545
552
  const c = usePaleta();
546
553
  const styles = useStyles();
554
+ const t = useTextos();
547
555
  if (cargando) return <ActivityIndicator color={c.primary} />;
548
- if (fallo)
549
- return <Text style={styles.linea}>No se pudo cargar. Jala la ficha para releer.</Text>;
556
+ if (fallo) return <Text style={styles.linea}>{t.ficha.seccionNoCargo}</Text>;
550
557
  return vacio ? <Text style={styles.linea}>{vacio}</Text> : null;
551
558
  }
552
559
 
@@ -10,6 +10,7 @@ import { setErrors } from "../formularios/errores";
10
10
  import TextoDeAccesoControlado from "../formularios/TextoDeAccesoControlado";
11
11
  import { crearEstilos, usePaleta } from "../tema";
12
12
  import { Text } from "../texto";
13
+ import { useTextos } from "../idioma";
13
14
 
14
15
  // Mismo corte que ContenidoDePantalla: de aquí para abajo es teléfono.
15
16
  const ANCHO_DE_TABLETA = 700;
@@ -58,6 +59,7 @@ export default function PantallaDeAcceso({
58
59
  const form = useForm<DatosDeAcceso>({ defaultValues: { username: "", password: "" } });
59
60
  const [entrando, setEntrando] = useState(false);
60
61
  const [aviso, setAviso] = useState<string | null>(null);
62
+ const t = useTextos();
61
63
 
62
64
  const entrar = form.handleSubmit(async (datos) => {
63
65
  setEntrando(true);
@@ -77,7 +79,7 @@ export default function PantallaDeAcceso({
77
79
  } catch {
78
80
  // `onEntrar` contesta lo que conoce; lo que se le escape no deja el botón cargando.
79
81
  Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
80
- setAviso("No se pudo iniciar sesión. Intente de nuevo.");
82
+ setAviso(t.acceso.noSePudo);
81
83
  } finally {
82
84
  setEntrando(false);
83
85
  }
@@ -97,13 +99,13 @@ export default function PantallaDeAcceso({
97
99
  <TextoDeAccesoControlado
98
100
  name="username"
99
101
  control={form.control}
100
- etiqueta="Usuario"
102
+ etiqueta={t.acceso.usuario}
101
103
  entrada={{ autoCapitalize: "none" }}
102
104
  />
103
105
  <TextoDeAccesoControlado
104
106
  name="password"
105
107
  control={form.control}
106
- etiqueta="Contraseña"
108
+ etiqueta={t.acceso.contrasena}
107
109
  contrasena
108
110
  entrada={{ autoCapitalize: "none" }}
109
111
  />
@@ -116,7 +118,7 @@ export default function PantallaDeAcceso({
116
118
  onPress={entrar}
117
119
  style={styles.boton}
118
120
  >
119
- Iniciar Sesión
121
+ {t.acceso.entrar}
120
122
  </Button>
121
123
  </>
122
124
  );
@@ -8,6 +8,7 @@ import EstadoVacio from "../componentes/EstadoVacio";
8
8
  import Nota from "../componentes/Nota";
9
9
  import { mensajeDeError } from "../formularios/errores";
10
10
  import { crearEstilos, usePaleta } from "../tema";
11
+ import { useTextos } from "../idioma";
11
12
 
12
13
  /**
13
14
  * El armazón de una pantalla que edita algo (un viaje, un tracto, una caja, un operador). Dos
@@ -43,19 +44,24 @@ export function EsperaDeEdicion({
43
44
  }) {
44
45
  const c = usePaleta();
45
46
  const styles = useStyles();
47
+ const t = useTextos();
46
48
  return (
47
49
  <View style={styles.root}>
48
50
  <CabeceraEmpujada eyebrow={eyebrow} titulo="…" />
49
51
  {!puede ? (
50
- <EstadoVacio icono="lock-outline" titulo="No tienes permiso" texto={sinPermiso} />
52
+ <EstadoVacio
53
+ icono="lock-outline"
54
+ titulo={t.edicion.sinPermiso}
55
+ texto={sinPermiso}
56
+ />
51
57
  ) : consulta.isError ? (
52
58
  <EstadoVacio
53
59
  icono="cloud-alert"
54
- titulo={`No se pudo cargar ${queEs}`}
55
- texto={mensajeDeError(consulta.error, "Revisa la señal e intenta de nuevo.")}
60
+ titulo={t.edicion.noSePudoCargar(queEs)}
61
+ texto={mensajeDeError(consulta.error, t.comun.revisaLaSenal)}
56
62
  >
57
63
  <BotonGrande icono="refresh" modo="contorno" onPress={consulta.refetch}>
58
- Reintentar
64
+ {t.comun.reintentar}
59
65
  </BotonGrande>
60
66
  </EstadoVacio>
61
67
  ) : (
@@ -71,7 +77,7 @@ export function FormularioDeEdicion({
71
77
  error,
72
78
  guardando,
73
79
  onGuardar,
74
- textoDeGuardar = "Guardar",
80
+ textoDeGuardar,
75
81
  children,
76
82
  }: {
77
83
  eyebrow: string;
@@ -86,6 +92,7 @@ export function FormularioDeEdicion({
86
92
  }) {
87
93
  const styles = useStyles();
88
94
  const insets = useSafeAreaInsets();
95
+ const t = useTextos();
89
96
  return (
90
97
  <View style={styles.root}>
91
98
  <CabeceraEmpujada eyebrow={eyebrow} titulo={titulo} />
@@ -99,7 +106,7 @@ export function FormularioDeEdicion({
99
106
  </ScrollView>
100
107
  <View style={[styles.pie, { paddingBottom: insets.bottom + 16 }]}>
101
108
  <BotonGrande icono="check" onPress={onGuardar} cargando={guardando}>
102
- {guardando ? "Guardando…" : textoDeGuardar}
109
+ {guardando ? t.edicion.guardando : (textoDeGuardar ?? t.edicion.guardar)}
103
110
  </BotonGrande>
104
111
  </View>
105
112
  </KeyboardAvoidingView>
@@ -6,6 +6,7 @@ import BotonGrande from "../componentes/BotonGrande";
6
6
  import CabeceraEmpujada from "../componentes/CabeceraEmpujada";
7
7
  import EstadoVacio from "../componentes/EstadoVacio";
8
8
  import { crearEstilos, usePaleta } from "../tema";
9
+ import { useTextos } from "../idioma";
9
10
 
10
11
  /** Lo que la ficha lee de un `use…Query` de RTK Query. */
11
12
  interface ConsultaDeFicha {
@@ -54,6 +55,7 @@ export default function PantallaDeFicha({
54
55
  const c = usePaleta();
55
56
  const styles = useStyles();
56
57
  const insets = useSafeAreaInsets();
58
+ const t = useTextos();
57
59
 
58
60
  let cuerpo: React.ReactNode;
59
61
  if (consulta.isLoading) {
@@ -62,17 +64,17 @@ export default function PantallaDeFicha({
62
64
  cuerpo = esNoEncontrado(consulta.error) ? (
63
65
  <EstadoVacio
64
66
  icono="file-hidden"
65
- titulo="No existe"
66
- texto="Puede que lo hayan borrado, o que no tengas permiso de verlo."
67
+ titulo={t.ficha.noExiste}
68
+ texto={t.ficha.noExisteTexto}
67
69
  />
68
70
  ) : (
69
71
  <EstadoVacio
70
72
  icono="cloud-off-outline"
71
- titulo="No se pudo cargar"
72
- texto="Revisa tu señal e intenta de nuevo."
73
+ titulo={t.comun.noSePudoCargar}
74
+ texto={t.comun.revisaLaSenal}
73
75
  >
74
76
  <BotonGrande icono="refresh" onPress={() => consulta.refetch()}>
75
- Reintentar
77
+ {t.comun.reintentar}
76
78
  </BotonGrande>
77
79
  </EstadoVacio>
78
80
  );
@@ -8,6 +8,7 @@ import EstadoVacio from "../componentes/EstadoVacio";
8
8
  import Tarjeta from "../componentes/Tarjeta";
9
9
  import { crearEstilos, renglon, tipografia, usePaleta, type Paleta } from "../tema";
10
10
  import { Text } from "../texto";
11
+ import { type Idioma, type Textos, useIdiomaDeLaLibreria, useTextos } from "../idioma";
11
12
 
12
13
  /**
13
14
  * Un movimiento de la bitácora de un registro, como lo responde cualquier endpoint `historial/`
@@ -64,20 +65,20 @@ const partirTipo = (tipo: string) => {
64
65
  };
65
66
 
66
67
  /** El valor de un campo tal como viene de la bitácora, listo para pintarse. */
67
- const formatear = (valor: unknown): string => {
68
+ const formatear = (valor: unknown, t: Textos): string => {
68
69
  if (valor === null || valor === undefined || valor === "") return "—";
69
- if (typeof valor === "boolean") return valor ? "Sí" : "No";
70
+ if (typeof valor === "boolean") return valor ? t.comun.si : t.comun.no;
70
71
  if (Array.isArray(valor)) return valor.length ? valor.join(", ") : "—";
71
72
  if (typeof valor === "object") return JSON.stringify(valor);
72
73
  return String(valor);
73
74
  };
74
75
 
75
76
  /** La fecha y hora del movimiento, en la hora del teléfono. */
76
- const fechaHora = (iso: string) => {
77
+ const fechaHora = (iso: string, idioma: Idioma) => {
77
78
  const fecha = new Date(iso);
78
79
  return Number.isNaN(fecha.getTime())
79
80
  ? "—"
80
- : fecha.toLocaleString("es-MX", {
81
+ : fecha.toLocaleString(idioma === "en" ? "en-US" : "es-MX", {
81
82
  day: "2-digit",
82
83
  month: "short",
83
84
  year: "numeric",
@@ -102,6 +103,8 @@ export function TarjetaDeEvento({
102
103
  }) {
103
104
  const c = usePaleta();
104
105
  const styles = useStyles();
106
+ const t = useTextos();
107
+ const idioma = useIdiomaDeLaLibreria();
105
108
  const [abierto, setAbierto] = React.useState(false);
106
109
 
107
110
  const { accion, modelo } = partirTipo(evento.history_type);
@@ -121,16 +124,18 @@ export function TarjetaDeEvento({
121
124
  <View style={styles.flex}>
122
125
  <Text style={styles.titulo}>{modelo}</Text>
123
126
  <Text style={styles.linea}>
124
- {[accion, evento.history_user || "Sistema"].filter(Boolean).join(" · ")}
127
+ {[accion, evento.history_user || t.historial.sistema]
128
+ .filter(Boolean)
129
+ .join(" · ")}
125
130
  </Text>
126
- <Text style={styles.linea}>{fechaHora(evento.history_date)}</Text>
131
+ <Text style={styles.linea}>{fechaHora(evento.history_date, idioma)}</Text>
127
132
  </View>
128
133
  </View>
129
134
 
130
135
  {/* Un arreglo vacío es «se guardó sin mover nada»; nulo es la creación, que no lista
131
136
  cambios. */}
132
137
  {!abierto && cambiados?.length === 0 ? (
133
- <Text style={styles.sinCambios}>Sin cambios en los campos.</Text>
138
+ <Text style={styles.sinCambios}>{t.historial.sinCambios}</Text>
134
139
  ) : null}
135
140
 
136
141
  {visibles.map((campo) => {
@@ -140,7 +145,7 @@ export function TarjetaDeEvento({
140
145
  <Text style={[styles.etiqueta, cambiado && styles.etiquetaCambiada]}>
141
146
  {labels[campo]}
142
147
  </Text>
143
- <Text style={styles.valor}>{formatear(evento[campo])}</Text>
148
+ <Text style={styles.valor}>{formatear(evento[campo], t)}</Text>
144
149
  </View>
145
150
  );
146
151
  })}
@@ -154,10 +159,10 @@ export function TarjetaDeEvento({
154
159
  >
155
160
  <Text style={styles.alternarTexto}>
156
161
  {abierto
157
- ? "Ocultar campos"
162
+ ? t.historial.ocultarCampos
158
163
  : cambiados?.length
159
- ? "Ver todos los campos"
160
- : "Ver los campos"}
164
+ ? t.historial.verTodos
165
+ : t.historial.verCampos}
161
166
  </Text>
162
167
  <MaterialCommunityIcons
163
168
  name={abierto ? "chevron-up" : "chevron-down"}
@@ -205,20 +210,21 @@ export default function PantallaDeHistorial({
205
210
  const insets = useSafeAreaInsets();
206
211
  const { data, isLoading, isFetching, isError, refetch } = consulta;
207
212
  const eventos = data?.history ?? [];
213
+ const t = useTextos();
208
214
 
209
215
  return (
210
216
  <View style={styles.root}>
211
- <CabeceraEmpujada eyebrow={eyebrow} titulo="Historial" />
217
+ <CabeceraEmpujada eyebrow={eyebrow} titulo={t.historial.titulo} />
212
218
  {/* El error sólo se queda con la pantalla cuando no hay nada que enseñar: un refresco
213
219
  que falla no debe borrar el historial que ya se estaba leyendo. */}
214
220
  {isError && !data ? (
215
221
  <EstadoVacio
216
222
  icono="cloud-off-outline"
217
- titulo="No se pudo cargar el historial"
218
- texto="Revisa tu señal e intenta de nuevo."
223
+ titulo={t.historial.noCargo}
224
+ texto={t.comun.revisaLaSenal}
219
225
  >
220
226
  <BotonGrande icono="refresh" modo="contorno" onPress={() => refetch()}>
221
- Reintentar
227
+ {t.comun.reintentar}
222
228
  </BotonGrande>
223
229
  </EstadoVacio>
224
230
  ) : isLoading ? (
@@ -240,7 +246,7 @@ export default function PantallaDeHistorial({
240
246
  }
241
247
  >
242
248
  {eventos.length === 0 ? (
243
- <EstadoVacio icono="history" titulo="Sin movimientos registrados" />
249
+ <EstadoVacio icono="history" titulo={t.historial.sinMovimientos} />
244
250
  ) : (
245
251
  eventos.map((evento, indice) => (
246
252
  // El backend no da una llave por evento (mezcla modelos con ids que se
@@ -13,16 +13,17 @@ import {
13
13
  type PreferenciaDeTema,
14
14
  } from "../tema";
15
15
  import { Text } from "../texto";
16
+ import { type Textos, useTextos } from "../idioma";
16
17
 
17
18
  /**
18
19
  * La apariencia: **igual que el teléfono** por omisión, o fija. Tres opciones y no un interruptor de
19
20
  * «modo oscuro», como el del portal: con un interruptor, quien lo toca una vez ya no vuelve a seguir
20
21
  * al teléfono.
21
22
  */
22
- const APARIENCIAS: Pestana<PreferenciaDeTema>[] = [
23
- { key: "sistema", label: "Teléfono", icono: "cellphone" },
24
- { key: "claro", label: "Claro", icono: "white-balance-sunny" },
25
- { key: "oscuro", label: "Oscuro", icono: "weather-night" },
23
+ const aparienciasDe = (t: Textos): Pestana<PreferenciaDeTema>[] => [
24
+ { key: "sistema", label: t.perfil.telefono, icono: "cellphone" },
25
+ { key: "claro", label: t.perfil.claro, icono: "white-balance-sunny" },
26
+ { key: "oscuro", label: t.perfil.oscuro, icono: "weather-night" },
26
27
  ];
27
28
 
28
29
  /**
@@ -52,10 +53,11 @@ export default function PantallaDePerfil({
52
53
  const styles = useStyles();
53
54
  const [tema, setTema] = usePreferenciaDeTema();
54
55
  const insets = useSafeAreaInsets();
56
+ const t = useTextos();
55
57
 
56
58
  return (
57
59
  <View style={styles.root}>
58
- <CabeceraEmpujada eyebrow={rotulo} titulo="Perfil" />
60
+ <CabeceraEmpujada eyebrow={rotulo} titulo={t.perfil.titulo} />
59
61
 
60
62
  <ContenidoDePantalla paddingBottom={insets.bottom + 20}>
61
63
  <ScrollView
@@ -66,13 +68,13 @@ export default function PantallaDePerfil({
66
68
  {/* Paper recorta título y subtítulo a un renglón por omisión: un nombre
67
69
  largo con la letra al 180% se perdía. */}
68
70
  <Card.Title
69
- title={nombre || "N/A"}
71
+ title={nombre || t.comun.noDisponible}
70
72
  titleVariant={"titleLarge"}
71
73
  titleStyle={{ fontWeight: "bold" }}
72
74
  titleNumberOfLines={0}
73
75
  subtitleNumberOfLines={0}
74
76
  subtitleStyle={renglon(tipografia.secundario)}
75
- subtitle={subtitulo || "N/A"}
77
+ subtitle={subtitulo || t.comun.noDisponible}
76
78
  subtitleVariant={"bodySmall"}
77
79
  left={(props) => (
78
80
  <Avatar.Text
@@ -85,8 +87,8 @@ export default function PantallaDePerfil({
85
87
  </Card>
86
88
 
87
89
  <View style={styles.apariencia}>
88
- <Text style={styles.rotulo}>Apariencia</Text>
89
- <Pestanas pestanas={APARIENCIAS} activa={tema} onSelect={setTema} />
90
+ <Text style={styles.rotulo}>{t.perfil.apariencia}</Text>
91
+ <Pestanas pestanas={aparienciasDe(t)} activa={tema} onSelect={setTema} />
90
92
  </View>
91
93
 
92
94
  {/* El tope de ancho vive en este View y no en el Button: el Surface de
@@ -100,7 +102,7 @@ export default function PantallaDePerfil({
100
102
  loading={saliendo}
101
103
  disabled={saliendo}
102
104
  >
103
- Cerrar sesión
105
+ {t.perfil.cerrarSesion}
104
106
  </Button>
105
107
  </View>
106
108
  {children}