@acvl/mobile-components 0.0.5 → 0.0.7

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 (119) hide show
  1. package/README.md +14 -0
  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/CabeceraEmpujada.js +3 -1
  11. package/lib/module/componentes/CabeceraEmpujada.js.map +1 -1
  12. package/lib/module/componentes/PantallaDeError.js +6 -4
  13. package/lib/module/componentes/PantallaDeError.js.map +1 -1
  14. package/lib/module/componentes/TiraDeFotos.js +4 -2
  15. package/lib/module/componentes/TiraDeFotos.js.map +1 -1
  16. package/lib/module/componentes/VisorDeFoto.js +6 -4
  17. package/lib/module/componentes/VisorDeFoto.js.map +1 -1
  18. package/lib/module/dialogos/HostDeDialogos.js +3 -1
  19. package/lib/module/dialogos/HostDeDialogos.js.map +1 -1
  20. package/lib/module/dialogos/dialogos.js +6 -4
  21. package/lib/module/dialogos/dialogos.js.map +1 -1
  22. package/lib/module/formularios/SelectorDeOpcion.js +13 -7
  23. package/lib/module/formularios/SelectorDeOpcion.js.map +1 -1
  24. package/lib/module/formularios/campos.js +9 -4
  25. package/lib/module/formularios/campos.js.map +1 -1
  26. package/lib/module/formularios/guardado.js +4 -2
  27. package/lib/module/formularios/guardado.js.map +1 -1
  28. package/lib/module/fotos/elegir.js +10 -8
  29. package/lib/module/fotos/elegir.js.map +1 -1
  30. package/lib/module/hoja/HojaDeLista.js +5 -3
  31. package/lib/module/hoja/HojaDeLista.js.map +1 -1
  32. package/lib/module/hoja/useMasDeLaFicha.js +3 -1
  33. package/lib/module/hoja/useMasDeLaFicha.js.map +1 -1
  34. package/lib/module/idioma/ProveedorDeIdioma.js +54 -0
  35. package/lib/module/idioma/ProveedorDeIdioma.js.map +1 -0
  36. package/lib/module/idioma/index.js +4 -0
  37. package/lib/module/idioma/index.js.map +1 -0
  38. package/lib/module/idioma/textos.js +239 -0
  39. package/lib/module/idioma/textos.js.map +1 -0
  40. package/lib/module/index.js +4 -0
  41. package/lib/module/index.js.map +1 -1
  42. package/lib/module/listas/HojaDeFiltros.js +11 -8
  43. package/lib/module/listas/HojaDeFiltros.js.map +1 -1
  44. package/lib/module/listas/ListaPaginada.js +6 -4
  45. package/lib/module/listas/ListaPaginada.js.map +1 -1
  46. package/lib/module/listas/piezas.js +13 -7
  47. package/lib/module/listas/piezas.js.map +1 -1
  48. package/lib/module/pantallas/PantallaDeAcceso.js +22 -7
  49. package/lib/module/pantallas/PantallaDeAcceso.js.map +1 -1
  50. package/lib/module/pantallas/PantallaDeEdicion.js +9 -6
  51. package/lib/module/pantallas/PantallaDeEdicion.js.map +1 -1
  52. package/lib/module/pantallas/PantallaDeFicha.js +7 -5
  53. package/lib/module/pantallas/PantallaDeFicha.js.map +1 -1
  54. package/lib/module/pantallas/PantallaDeHistorial.js +18 -14
  55. package/lib/module/pantallas/PantallaDeHistorial.js.map +1 -1
  56. package/lib/module/pantallas/PantallaDePerfil.js +12 -10
  57. package/lib/module/pantallas/PantallaDePerfil.js.map +1 -1
  58. package/lib/typescript/src/camara/CampoDeFotos.d.ts.map +1 -1
  59. package/lib/typescript/src/camara/elegir.d.ts.map +1 -1
  60. package/lib/typescript/src/componentes/AccionesDelEncabezado.d.ts.map +1 -1
  61. package/lib/typescript/src/componentes/BotonDeAvatar.d.ts.map +1 -1
  62. package/lib/typescript/src/componentes/CabeceraEmpujada.d.ts.map +1 -1
  63. package/lib/typescript/src/componentes/PantallaDeError.d.ts.map +1 -1
  64. package/lib/typescript/src/componentes/TiraDeFotos.d.ts.map +1 -1
  65. package/lib/typescript/src/componentes/VisorDeFoto.d.ts.map +1 -1
  66. package/lib/typescript/src/dialogos/HostDeDialogos.d.ts.map +1 -1
  67. package/lib/typescript/src/dialogos/dialogos.d.ts.map +1 -1
  68. package/lib/typescript/src/formularios/SelectorDeOpcion.d.ts.map +1 -1
  69. package/lib/typescript/src/formularios/campos.d.ts.map +1 -1
  70. package/lib/typescript/src/formularios/guardado.d.ts.map +1 -1
  71. package/lib/typescript/src/fotos/elegir.d.ts.map +1 -1
  72. package/lib/typescript/src/hoja/HojaDeLista.d.ts.map +1 -1
  73. package/lib/typescript/src/hoja/useMasDeLaFicha.d.ts.map +1 -1
  74. package/lib/typescript/src/idioma/ProveedorDeIdioma.d.ts +16 -0
  75. package/lib/typescript/src/idioma/ProveedorDeIdioma.d.ts.map +1 -0
  76. package/lib/typescript/src/idioma/index.d.ts +3 -0
  77. package/lib/typescript/src/idioma/index.d.ts.map +1 -0
  78. package/lib/typescript/src/idioma/textos.d.ts +125 -0
  79. package/lib/typescript/src/idioma/textos.d.ts.map +1 -0
  80. package/lib/typescript/src/index.d.ts +1 -0
  81. package/lib/typescript/src/index.d.ts.map +1 -1
  82. package/lib/typescript/src/listas/HojaDeFiltros.d.ts.map +1 -1
  83. package/lib/typescript/src/listas/ListaPaginada.d.ts.map +1 -1
  84. package/lib/typescript/src/listas/piezas.d.ts.map +1 -1
  85. package/lib/typescript/src/pantallas/PantallaDeAcceso.d.ts +5 -2
  86. package/lib/typescript/src/pantallas/PantallaDeAcceso.d.ts.map +1 -1
  87. package/lib/typescript/src/pantallas/PantallaDeEdicion.d.ts.map +1 -1
  88. package/lib/typescript/src/pantallas/PantallaDeFicha.d.ts.map +1 -1
  89. package/lib/typescript/src/pantallas/PantallaDeHistorial.d.ts.map +1 -1
  90. package/lib/typescript/src/pantallas/PantallaDePerfil.d.ts.map +1 -1
  91. package/package.json +1 -1
  92. package/src/camara/CampoDeFotos.tsx +4 -2
  93. package/src/camara/elegir.ts +4 -3
  94. package/src/componentes/AccionesDelEncabezado.tsx +3 -1
  95. package/src/componentes/BotonDeAvatar.tsx +22 -18
  96. package/src/componentes/CabeceraEmpujada.tsx +3 -1
  97. package/src/componentes/PantallaDeError.tsx +6 -4
  98. package/src/componentes/TiraDeFotos.tsx +4 -2
  99. package/src/componentes/VisorDeFoto.tsx +7 -7
  100. package/src/dialogos/HostDeDialogos.tsx +3 -1
  101. package/src/dialogos/dialogos.ts +15 -4
  102. package/src/formularios/SelectorDeOpcion.tsx +22 -8
  103. package/src/formularios/campos.tsx +9 -4
  104. package/src/formularios/guardado.ts +4 -2
  105. package/src/fotos/elegir.ts +14 -10
  106. package/src/hoja/HojaDeLista.tsx +5 -5
  107. package/src/hoja/useMasDeLaFicha.tsx +3 -1
  108. package/src/idioma/ProveedorDeIdioma.tsx +51 -0
  109. package/src/idioma/index.ts +7 -0
  110. package/src/idioma/textos.ts +248 -0
  111. package/src/index.ts +11 -0
  112. package/src/listas/HojaDeFiltros.tsx +10 -9
  113. package/src/listas/ListaPaginada.tsx +6 -6
  114. package/src/listas/piezas.tsx +17 -10
  115. package/src/pantallas/PantallaDeAcceso.tsx +24 -5
  116. package/src/pantallas/PantallaDeEdicion.tsx +13 -6
  117. package/src/pantallas/PantallaDeFicha.tsx +7 -5
  118. package/src/pantallas/PantallaDeHistorial.tsx +22 -16
  119. package/src/pantallas/PantallaDePerfil.tsx +12 -10
@@ -1 +1 @@
1
- {"version":3,"file":"PantallaDePerfil.d.ts","sourceRoot":"","sources":["../../../../src/pantallas/PantallaDePerfil.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AA2B1B;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,SAAS,EACT,cAAc,EACd,QAAgB,EAChB,QAAQ,GACX,EAAE;IACC,yFAAyF;IACzF,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC9B,qBA4DA"}
1
+ {"version":3,"file":"PantallaDePerfil.d.ts","sourceRoot":"","sources":["../../../../src/pantallas/PantallaDePerfil.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AA4B1B;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,SAAS,EACT,cAAc,EACd,QAAgB,EAChB,QAAQ,GACX,EAAE;IACC,yFAAyF;IACzF,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC9B,qBA6DA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acvl/mobile-components",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Librería de componentes de las apps móviles de ACV (Expo / React Native).",
5
5
  "homepage": "https://github.com/ACV-Logistics/acv-mobile-components#readme",
6
6
  "bugs": {
@@ -13,6 +13,7 @@ import {
13
13
  type ArchivoElegido,
14
14
  type PermisoDeCamara,
15
15
  } from "./elegir";
16
+ import { useTextos } from "../idioma";
16
17
 
17
18
  /**
18
19
  * Una foto que ya está en el servidor (la de una captura que se vuelve a abrir): se enseña con su
@@ -68,6 +69,7 @@ export function CampoDeFotos({
68
69
  disabled?: boolean;
69
70
  }) {
70
71
  const styles = useStyles();
72
+ const t = useTextos();
71
73
  // Reducir una foto tarda (se decodifica completa): el botón gira mientras tanto.
72
74
  const [eligiendo, setEligiendo] = React.useState<"camara" | "galeria" | null>(null);
73
75
 
@@ -116,7 +118,7 @@ export function CampoDeFotos({
116
118
  disabled={disabled || eligiendo !== null}
117
119
  onPress={() => void agregar("camara")}
118
120
  >
119
- Tomar foto
121
+ {t.fotos.tomarFoto}
120
122
  </BotonGrande>
121
123
  <BotonGrande
122
124
  icono="image-outline"
@@ -125,7 +127,7 @@ export function CampoDeFotos({
125
127
  disabled={disabled || eligiendo !== null}
126
128
  onPress={() => void agregar("galeria")}
127
129
  >
128
- De la galería
130
+ {t.fotos.deLaGaleria}
129
131
  </BotonGrande>
130
132
  </View>
131
133
  </View>
@@ -4,6 +4,7 @@
4
4
  import * as ImagePicker from "expo-image-picker";
5
5
  import { avisar } from "../dialogos/dialogos";
6
6
  import { reducirFoto } from "./reducir";
7
+ import { textosActuales } from "../idioma";
7
8
 
8
9
  /** Lo elegido: dónde está en el teléfono, su nombre, su tipo y si es una foto. */
9
10
  export interface ArchivoElegido {
@@ -43,15 +44,15 @@ export async function pedirPermisoDeCamara({
43
44
  if (actual.granted) return true;
44
45
  if (!actual.canAskAgain) {
45
46
  await avisar({
46
- titulo: "Sin permiso de cámara",
47
- mensaje: `Para ${para}, activa la cámara para la app en los ajustes del teléfono.`,
47
+ titulo: textosActuales().fotos.sinPermisoDeCamara,
48
+ mensaje: textosActuales().fotos.activaLaCamara(para),
48
49
  icono: "camera-off-outline",
49
50
  });
50
51
  return false;
51
52
  }
52
53
  if (explicacion) {
53
54
  await avisar({
54
- titulo: "La cámara",
55
+ titulo: textosActuales().fotos.laCamara,
55
56
  mensaje: explicacion,
56
57
  icono: "camera-outline",
57
58
  cancelable: false,
@@ -11,6 +11,7 @@ import {
11
11
  type LayoutChangeEvent,
12
12
  } from "react-native";
13
13
  import { Menu } from "react-native-paper";
14
+ import { useTextos } from "../idioma";
14
15
 
15
16
  /** Lo que mide un botón y lo que se separan: con eso se cuenta cuántos caben. */
16
17
  const LADO = 40;
@@ -106,6 +107,7 @@ const Boton = ({
106
107
  */
107
108
  export default function AccionesDelEncabezado({ acciones }: { acciones: AccionDelEncabezado[] }) {
108
109
  const c = usePaleta();
110
+ const t = useTextos();
109
111
  const styles = useStyles();
110
112
  const [hueco, setHueco] = React.useState<number | null>(null);
111
113
  const [menuAbierto, setMenuAbierto] = React.useState(false);
@@ -160,7 +162,7 @@ export default function AccionesDelEncabezado({ acciones }: { acciones: AccionDe
160
162
  anchor={
161
163
  <Boton
162
164
  accion={{
163
- etiqueta: "Más acciones",
165
+ etiqueta: t.listas.masAcciones,
164
166
  icono: "dots-vertical",
165
167
  insignia: escondidas,
166
168
  }}
@@ -1,6 +1,7 @@
1
1
  import { Pressable, StyleSheet } from "react-native";
2
2
  import { Avatar } from "react-native-paper";
3
3
  import { tipografia } from "../tema";
4
+ import { useTextos } from "../idioma";
4
5
 
5
6
  /**
6
7
  * La inicial de cada parte, hasta dos: `inicialesDe(nombre, apellidos)` da «JP»; con sólo el nombre
@@ -22,29 +23,32 @@ export const inicialesDe = (...partes: (string | null | undefined)[]): string =>
22
23
  const BotonDeAvatar = ({
23
24
  iniciales,
24
25
  onPress,
25
- etiqueta = "Perfil",
26
+ etiqueta,
26
27
  }: {
27
28
  iniciales: string;
28
29
  onPress: () => void;
29
30
  /** Lo que lee el lector de pantalla. */
30
31
  etiqueta?: string;
31
- }) => (
32
- <Pressable
33
- onPress={onPress}
34
- hitSlop={8}
35
- accessibilityRole="button"
36
- accessibilityLabel={etiqueta}
37
- style={({ pressed }) => [styles.btn, pressed && { opacity: 0.6 }]}
38
- >
39
- <Avatar.Text
40
- size={36}
41
- label={iniciales.slice(0, 2).toUpperCase()}
42
- // Blanca sobre el rojo en los dos modos: el tema oscuro de Paper la ponía oscura.
43
- color="#ffffff"
44
- labelStyle={styles.initial}
45
- />
46
- </Pressable>
47
- );
32
+ }) => {
33
+ const t = useTextos();
34
+ return (
35
+ <Pressable
36
+ onPress={onPress}
37
+ hitSlop={8}
38
+ accessibilityRole="button"
39
+ accessibilityLabel={etiqueta ?? t.perfil.titulo}
40
+ style={({ pressed }) => [styles.btn, pressed && { opacity: 0.6 }]}
41
+ >
42
+ <Avatar.Text
43
+ size={36}
44
+ label={iniciales.slice(0, 2).toUpperCase()}
45
+ // Blanca sobre el rojo en los dos modos: el tema oscuro de Paper la ponía oscura.
46
+ color="#ffffff"
47
+ labelStyle={styles.initial}
48
+ />
49
+ </Pressable>
50
+ );
51
+ };
48
52
 
49
53
  const styles = StyleSheet.create({
50
54
  btn: {
@@ -8,6 +8,7 @@ import { router } from "expo-router";
8
8
  import type React from "react";
9
9
  import { TouchableOpacity, View } from "react-native";
10
10
  import { useSafeAreaInsets } from "react-native-safe-area-context";
11
+ import { useTextos } from "../idioma";
11
12
 
12
13
  /**
13
14
  * La cabecera de una pantalla empujada encima de las pestañas: atrás, el rótulo, el título y, si
@@ -31,10 +32,11 @@ const CabeceraEmpujada = ({
31
32
  const c = usePaleta();
32
33
  const styles = useStyles();
33
34
  const insets = useSafeAreaInsets();
35
+ const t = useTextos();
34
36
  return (
35
37
  <View style={[styles.hdr, { paddingTop: insets.top + 10 }]}>
36
38
  <View style={styles.hdrRow}>
37
- <TouchableOpacity onPress={() => router.back()} accessibilityLabel="Atrás">
39
+ <TouchableOpacity onPress={() => router.back()} accessibilityLabel={t.comun.atras}>
38
40
  <MaterialIcons name="arrow-back" size={45} color={c.primary} />
39
41
  </TouchableOpacity>
40
42
  {/* `flexShrink` y no `flex`: con acciones, el título les deja su hueco. */}
@@ -5,6 +5,7 @@ import { SafeAreaView } from "react-native-safe-area-context";
5
5
  import { Text } from "../texto";
6
6
  import { crearEstilos, tipografia, usePaleta } from "../tema";
7
7
  import BotonGrande from "./BotonGrande";
8
+ import { useTextos } from "../idioma";
8
9
 
9
10
  /** Lo que expo-router le pasa al `ErrorBoundary` de un layout. */
10
11
  export interface PropsDePantallaDeError {
@@ -27,7 +28,7 @@ const PantallaDeError = ({
27
28
  error,
28
29
  retry,
29
30
  onError,
30
- mensaje = "Esta pantalla no se pudo abrir. Intenta de nuevo; si vuelve a pasar, avisa a soporte.",
31
+ mensaje,
31
32
  }: PropsDePantallaDeError & {
32
33
  onError?: (error: Error) => void;
33
34
  /** Lo que se le dice a quien la ve: qué no se perdió y a quién avisar. */
@@ -35,6 +36,7 @@ const PantallaDeError = ({
35
36
  }) => {
36
37
  const c = usePaleta();
37
38
  const styles = useStyles();
39
+ const t = useTextos();
38
40
  useEffect(() => {
39
41
  onError?.(error);
40
42
  // Se reporta una vez por error, aunque quien llama pase un `onError` nuevo en cada render.
@@ -45,10 +47,10 @@ const PantallaDeError = ({
45
47
  <SafeAreaView style={styles.root}>
46
48
  <ScrollView contentContainerStyle={styles.contenido}>
47
49
  <MaterialCommunityIcons name="alert-octagon-outline" size={64} color={c.primary} />
48
- <Text style={styles.titulo}>Algo salió mal</Text>
49
- <Text style={styles.texto}>{mensaje}</Text>
50
+ <Text style={styles.titulo}>{t.error.titulo}</Text>
51
+ <Text style={styles.texto}>{mensaje ?? t.error.mensaje}</Text>
50
52
  <BotonGrande icono="refresh" onPress={() => void retry()}>
51
- Reintentar
53
+ {t.comun.reintentar}
52
54
  </BotonGrande>
53
55
  </ScrollView>
54
56
  </SafeAreaView>
@@ -5,6 +5,7 @@ import { crearEstilos, tipografia, usePaleta } from "../tema";
5
5
  import { Text } from "../texto";
6
6
  import Foto, { type FotoAVer } from "./Foto";
7
7
  import VisorDeFoto from "./VisorDeFoto";
8
+ import { useTextos } from "../idioma";
8
9
 
9
10
  /** Una miniatura de la tira: qué se pinta, qué abre el visor y qué se puede hacer con ella. */
10
11
  export interface FotoDeLaTira {
@@ -41,6 +42,7 @@ interface Props {
41
42
  */
42
43
  const TiraDeFotos = ({ fotos: piezas, lado, cuadricula = false }: Props) => {
43
44
  const styles = useStyles();
45
+ const t = useTextos();
44
46
  const [viendo, setViendo] = React.useState<number | null>(null);
45
47
  if (piezas.length === 0) return null;
46
48
 
@@ -56,7 +58,7 @@ const TiraDeFotos = ({ fotos: piezas, lado, cuadricula = false }: Props) => {
56
58
  onPress={() => setViendo(fotos.indexOf(p))}
57
59
  accessibilityRole={p.esFoto ? "imagebutton" : undefined}
58
60
  accessibilityLabel={
59
- p.etiqueta ?? (p.esFoto ? "Ver la foto" : `Archivo ${p.nombre ?? ""}`.trim())
61
+ p.etiqueta ?? (p.esFoto ? t.fotos.verLaFoto : t.fotos.archivo(p.nombre ?? ""))
60
62
  }
61
63
  >
62
64
  {p.esFoto ? (
@@ -84,7 +86,7 @@ const TiraDeFotos = ({ fotos: piezas, lado, cuadricula = false }: Props) => {
84
86
  onPress={p.onQuitar}
85
87
  hitSlop={10}
86
88
  accessibilityRole="button"
87
- accessibilityLabel={p.esFoto ? "Quitar esta foto" : "Quitar este archivo"}
89
+ accessibilityLabel={p.esFoto ? t.fotos.quitarFoto : t.fotos.quitarArchivo}
88
90
  style={styles.quitar}
89
91
  >
90
92
  <MaterialCommunityIcons name="close" size={24} color="#fff" />
@@ -13,6 +13,7 @@ import {
13
13
  } from "react-native";
14
14
  import { useSafeAreaInsets } from "react-native-safe-area-context";
15
15
  import Foto, { type FotoAVer } from "./Foto";
16
+ import { useTextos } from "../idioma";
16
17
 
17
18
  export type { FotoAVer };
18
19
 
@@ -78,6 +79,7 @@ function Carrusel({
78
79
  }) {
79
80
  const insets = useSafeAreaInsets();
80
81
  const { width, height } = useWindowDimensions();
82
+ const t = useTextos();
81
83
  const primera = Math.min(inicio, Math.max(0, fotos.length - 1));
82
84
  const [actual, setActual] = React.useState(primera);
83
85
  const [descarga, setDescarga] = React.useState<Descarga>("nada");
@@ -121,9 +123,7 @@ function Carrusel({
121
123
 
122
124
  <View style={[styles.barra, { top: insets.top + 12 }]}>
123
125
  {fotos.length > 1 ? (
124
- <Text style={styles.cuenta}>
125
- {actual + 1} de {fotos.length}
126
- </Text>
126
+ <Text style={styles.cuenta}>{t.fotos.deTotal(actual + 1, fotos.length)}</Text>
127
127
  ) : (
128
128
  <View />
129
129
  )}
@@ -135,10 +135,10 @@ function Carrusel({
135
135
  accessibilityRole="button"
136
136
  accessibilityLabel={
137
137
  descarga === "lista"
138
- ? "Guardada en la galería"
138
+ ? t.fotos.guardada
139
139
  : descarga === "fallo"
140
- ? "No se pudo guardar; intentar de nuevo"
141
- : "Guardar en la galería"
140
+ ? t.fotos.noSeGuardo
141
+ : t.fotos.guardarEnGaleria
142
142
  }
143
143
  accessibilityState={{ busy: descarga === "bajando" }}
144
144
  >
@@ -153,7 +153,7 @@ function Carrusel({
153
153
  )}
154
154
  </Pressable>
155
155
  ) : null}
156
- <Pressable onPress={onCerrar} hitSlop={12} accessibilityLabel="Cerrar">
156
+ <Pressable onPress={onCerrar} hitSlop={12} accessibilityLabel={t.comun.cerrar}>
157
157
  <MaterialCommunityIcons name="close" size={32} color="#fff" />
158
158
  </Pressable>
159
159
  </View>
@@ -10,6 +10,7 @@ import {
10
10
  import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
11
11
  import React from "react";
12
12
  import { Modal, Pressable, ScrollView, useWindowDimensions, View } from "react-native";
13
+ import { useTextos } from "../idioma";
13
14
 
14
15
  /**
15
16
  * Pinta los diálogos que abre `mostrarDialogo` (`dialogos.ts`), de uno en uno. Se monta una
@@ -23,6 +24,7 @@ import { Modal, Pressable, ScrollView, useWindowDimensions, View } from "react-n
23
24
  */
24
25
  const HostDeDialogos = () => {
25
26
  const c = usePaleta();
27
+ const t = useTextos();
26
28
  const styles = useStyles();
27
29
  const dialogo = React.useSyncExternalStore(suscribirDialogos, dialogoActual);
28
30
  const { height } = useWindowDimensions();
@@ -45,7 +47,7 @@ const HostDeDialogos = () => {
45
47
  navigationBarTranslucent
46
48
  onRequestClose={cerrar}
47
49
  >
48
- <Pressable style={styles.fondo} onPress={cerrar} accessibilityLabel="Cerrar">
50
+ <Pressable style={styles.fondo} onPress={cerrar} accessibilityLabel={t.comun.cerrar}>
49
51
  {/* El `Pressable` vacío de la tarjeta se queda con el toque: tocar adentro no cierra. */}
50
52
  <Pressable
51
53
  style={[styles.tarjeta, { maxHeight: height * 0.85 }]}
@@ -3,6 +3,7 @@
3
3
  // navegación). Los pinta `HostDeDialogos`, montado una vez en el layout raíz.
4
4
  import type MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
5
5
  import type React from "react";
6
+ import { textosActuales } from "../idioma";
6
7
 
7
8
  type NombreDeIcono = React.ComponentProps<typeof MaterialCommunityIcons>["name"];
8
9
 
@@ -73,7 +74,7 @@ export async function confirmar({
73
74
  titulo,
74
75
  mensaje,
75
76
  textoConfirmar,
76
- textoCancelar = "Cancelar",
77
+ textoCancelar,
77
78
  destructivo = false,
78
79
  icono,
79
80
  }: {
@@ -94,7 +95,11 @@ export async function confirmar({
94
95
  estilo: destructivo ? "destructivo" : "principal",
95
96
  valor: true,
96
97
  },
97
- { texto: textoCancelar, estilo: "cancelar", valor: false },
98
+ {
99
+ texto: textoCancelar ?? textosActuales().comun.cancelar,
100
+ estilo: "cancelar",
101
+ valor: false,
102
+ },
98
103
  ],
99
104
  });
100
105
  return respuesta === true;
@@ -105,7 +110,7 @@ export async function avisar({
105
110
  titulo,
106
111
  mensaje,
107
112
  icono = "information-outline",
108
- textoBoton = "Entendido",
113
+ textoBoton,
109
114
  cancelable = true,
110
115
  }: {
111
116
  titulo: string;
@@ -119,7 +124,13 @@ export async function avisar({
119
124
  mensaje,
120
125
  icono,
121
126
  cancelable,
122
- botones: [{ texto: textoBoton, estilo: "principal", valor: true }],
127
+ botones: [
128
+ {
129
+ texto: textoBoton ?? textosActuales().comun.entendido,
130
+ estilo: "principal",
131
+ valor: true,
132
+ },
133
+ ],
123
134
  });
124
135
  }
125
136
 
@@ -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 =