@acvl/mobile-components 0.0.5 → 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 (118) 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 +237 -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 +6 -4
  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 +124 -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.map +1 -1
  86. package/lib/typescript/src/pantallas/PantallaDeEdicion.d.ts.map +1 -1
  87. package/lib/typescript/src/pantallas/PantallaDeFicha.d.ts.map +1 -1
  88. package/lib/typescript/src/pantallas/PantallaDeHistorial.d.ts.map +1 -1
  89. package/lib/typescript/src/pantallas/PantallaDePerfil.d.ts.map +1 -1
  90. package/package.json +1 -1
  91. package/src/camara/CampoDeFotos.tsx +4 -2
  92. package/src/camara/elegir.ts +4 -3
  93. package/src/componentes/AccionesDelEncabezado.tsx +3 -1
  94. package/src/componentes/BotonDeAvatar.tsx +22 -18
  95. package/src/componentes/CabeceraEmpujada.tsx +3 -1
  96. package/src/componentes/PantallaDeError.tsx +6 -4
  97. package/src/componentes/TiraDeFotos.tsx +4 -2
  98. package/src/componentes/VisorDeFoto.tsx +7 -7
  99. package/src/dialogos/HostDeDialogos.tsx +3 -1
  100. package/src/dialogos/dialogos.ts +15 -4
  101. package/src/formularios/SelectorDeOpcion.tsx +22 -8
  102. package/src/formularios/campos.tsx +9 -4
  103. package/src/formularios/guardado.ts +4 -2
  104. package/src/fotos/elegir.ts +14 -10
  105. package/src/hoja/HojaDeLista.tsx +5 -5
  106. package/src/hoja/useMasDeLaFicha.tsx +3 -1
  107. package/src/idioma/ProveedorDeIdioma.tsx +51 -0
  108. package/src/idioma/index.ts +7 -0
  109. package/src/idioma/textos.ts +246 -0
  110. package/src/index.ts +11 -0
  111. package/src/listas/HojaDeFiltros.tsx +10 -9
  112. package/src/listas/ListaPaginada.tsx +6 -6
  113. package/src/listas/piezas.tsx +17 -10
  114. package/src/pantallas/PantallaDeAcceso.tsx +6 -4
  115. package/src/pantallas/PantallaDeEdicion.tsx +13 -6
  116. package/src/pantallas/PantallaDeFicha.tsx +7 -5
  117. package/src/pantallas/PantallaDeHistorial.tsx +22 -16
  118. package/src/pantallas/PantallaDePerfil.tsx +12 -10
@@ -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
@@ -151,3 +151,14 @@ export {
151
151
  type Historial,
152
152
  } from "./pantallas/PantallaDeHistorial";
153
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";
@@ -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
  );