@anchrd/intel-ui 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/agent/agent-calendar/agent-calendar.tsx +32 -14
- package/src/agent/agent-chat/agent-chat.tsx +3 -1
- package/src/agent/agent-cron/agent-cron.ts +49 -113
- package/src/agent/agent-definition/agent-definition.ts +18 -9
- package/src/agent/agent-entry-title/agent-entry-title.ts +3 -1
- package/src/agent/agent-log/agent-log.tsx +6 -3
- package/src/agent/agent-profile/agent-profile.tsx +46 -12
- package/src/agent/agent.tsx +17 -9
- package/src/app/app-sidebar/app-sidebar.tsx +2 -2
- package/src/app/app-tree/app-tree.tsx +5 -3
- package/src/app/app.tsx +2 -2
- package/src/app/header-search/header-search.tsx +3 -1
- package/src/app/settings-dialog/settings-dialog.tsx +133 -0
- package/src/app/tree-move/tree-move.tsx +3 -1
- package/src/app/user-footer/user-footer.tsx +25 -2
- package/src/app/view-toggle/view-toggle.tsx +2 -2
- package/src/archive/archive.tsx +3 -1
- package/src/entry-picker/entry-picker.tsx +3 -1
- package/src/flow-runs/flow-runs.tsx +6 -3
- package/src/flows/flows.tsx +13 -9
- package/src/flows/node-palette/node-palette.tsx +2 -2
- package/src/folder-contents/folder-contents.tsx +3 -1
- package/src/graph-pane/graph-pane.tsx +2 -2
- package/src/i18n/de.json +402 -0
- package/src/i18n/en.json +16 -5
- package/src/i18n/es.json +402 -0
- package/src/i18n/i18n-context.tsx +65 -0
- package/src/i18n/i18n-languages/i18n-languages.ts +83 -0
- package/src/i18n/i18n.ts +15 -11
- package/src/i18n/i18n.types.ts +43 -0
- package/src/main.tsx +33 -12
- package/src/modal/modal.tsx +2 -2
- package/src/node-table/node-table.tsx +3 -1
- package/src/nodes/nodes.tsx +7 -3
- package/src/resource-menu/resource-menu.tsx +8 -4
- package/src/router/router.types.ts +3 -2
- package/src/save-button/save-button.tsx +3 -3
- package/src/sign-in-refused/sign-in-refused.tsx +3 -2
- package/src/theme/theme-context.tsx +89 -0
- package/src/theme/theme.ts +51 -21
- package/src/timezone/timezone-context.tsx +45 -0
- package/src/timezone/timezone.ts +60 -0
- package/src/tools/tools.tsx +3 -1
package/src/i18n/es.json
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$locale": "es-ES",
|
|
3
|
+
"app.name": "Intel",
|
|
4
|
+
"nav.primary": "Navegación principal",
|
|
5
|
+
"nav.nodes": "Conocimiento",
|
|
6
|
+
"nav.flows": "Flujos",
|
|
7
|
+
"nav.tools": "Herramientas",
|
|
8
|
+
"shell.toggleSidebar": "Plegar o desplegar la barra lateral",
|
|
9
|
+
"shell.resizeSidebar": "Ancho de la barra lateral",
|
|
10
|
+
"shell.signedInUser": "Persona conectada",
|
|
11
|
+
"shell.userUnavailable": "Cuenta no disponible",
|
|
12
|
+
"shell.userMenu": "Cuenta y herramientas de {name}",
|
|
13
|
+
"search.open": "Buscar",
|
|
14
|
+
"search.title": "Buscar en Intel",
|
|
15
|
+
"search.description": "Encuentra documentos, flujos y herramientas que puedes ver.",
|
|
16
|
+
"search.placeholder": "Buscar documentos, flujos y herramientas",
|
|
17
|
+
"search.filter": "Filtrar por tipo",
|
|
18
|
+
"search.all": "Todo",
|
|
19
|
+
"search.kind.node": "Conocimiento",
|
|
20
|
+
"search.kind.flow": "Flujos",
|
|
21
|
+
"search.kind.tool": "Herramientas",
|
|
22
|
+
"search.hint": "Escribe para buscar.",
|
|
23
|
+
"search.searching": "Buscando…",
|
|
24
|
+
"search.empty": "No se ha encontrado nada para «{query}».",
|
|
25
|
+
"search.failed": "La búsqueda no se ha podido completar.",
|
|
26
|
+
"tree.label": "Conocimiento y flujos",
|
|
27
|
+
"tree.contents": "Dentro de {title}",
|
|
28
|
+
"tree.calls": "Flujos a los que llama {title}",
|
|
29
|
+
"tree.empty": "Aquí todavía no hay nada.",
|
|
30
|
+
"tree.failed": "Esta parte del árbol no se ha podido cargar.",
|
|
31
|
+
"tree.expand": "Mostrar lo que hay dentro de {title}",
|
|
32
|
+
"tree.collapse": "Ocultar lo que hay dentro de {title}",
|
|
33
|
+
"tree.expandCalls": "Mostrar los flujos a los que llama {title}",
|
|
34
|
+
"tree.collapseCalls": "Ocultar los flujos a los que llama {title}",
|
|
35
|
+
"tree.add": "Añadir a {title}",
|
|
36
|
+
"tree.addRoot": "Añadir en el nivel superior",
|
|
37
|
+
"tree.new.folder": "Carpeta nueva",
|
|
38
|
+
"tree.new.document": "Documento nuevo",
|
|
39
|
+
"tree.new.table": "Tabla nueva",
|
|
40
|
+
"tree.new.upload": "Subir un archivo",
|
|
41
|
+
"tree.new.flow": "Flujo nuevo",
|
|
42
|
+
"tree.new.agent": "Agente nuevo",
|
|
43
|
+
"tree.upload": "Archivo que subir",
|
|
44
|
+
"tree.table.columns": "Columnas",
|
|
45
|
+
"tree.table.columnsHint": "Separa los nombres de las columnas con comas. Son la cabecera, y toda fila añadida después tiene que encajar con ella.",
|
|
46
|
+
"tree.createFailed": "No se ha podido crear. Comprueba tu acceso e inténtalo de nuevo.",
|
|
47
|
+
"tree.uploadFailed": "El archivo no se ha podido subir. Comprueba el límite de 15 MB e inténtalo de nuevo.",
|
|
48
|
+
"tree.kind.folder": "Carpeta",
|
|
49
|
+
"tree.kind.document": "Documento",
|
|
50
|
+
"tree.kind.attachment": "Archivo",
|
|
51
|
+
"tree.kind.table": "Tabla",
|
|
52
|
+
"tree.kind.agent": "Agente",
|
|
53
|
+
"tree.kind.flow": "Flujo",
|
|
54
|
+
"tree.move.action": "Mover a…",
|
|
55
|
+
"tree.move.title": "Mover {title}",
|
|
56
|
+
"tree.move.root": "Nivel superior",
|
|
57
|
+
"tree.move.dropRoot": "Suelta aquí para mover al nivel superior",
|
|
58
|
+
"tree.move.pick": "Elige la carpeta a la que moverlo.",
|
|
59
|
+
"tree.move.up": "Volver a la carpeta de arriba",
|
|
60
|
+
"tree.move.open": "Abrir {title}",
|
|
61
|
+
"tree.move.noFolders": "Aquí no hay ninguna carpeta a la que moverlo.",
|
|
62
|
+
"tree.move.here": "Mover dentro de {title}",
|
|
63
|
+
"tree.move.alreadyThere": "Ya está archivado aquí.",
|
|
64
|
+
"tree.move.destination": "Sitio nuevo: {title}",
|
|
65
|
+
"tree.move.sharingWarning": "El acceso sigue a la carpeta. Después de moverlo, todo el que llegue a {title} llegará a esta entrada, y quien solo llegaba a ella por su carpeta anterior la perderá. No se avisa a nadie.",
|
|
66
|
+
"tree.move.elsewhere": "Elegir otra carpeta",
|
|
67
|
+
"tree.move.confirm": "Mover",
|
|
68
|
+
"tree.move.failed.forbidden": "No se ha movido: no puedes escribir en esa carpeta. Pide acceso de escritura allí.",
|
|
69
|
+
"tree.move.failed.notFolder": "No se ha movido: solo una carpeta puede contener otras entradas.",
|
|
70
|
+
"tree.move.failed.cycle": "No se ha movido: una carpeta no puede ir dentro de sí misma ni dentro de nada que contenga.",
|
|
71
|
+
"tree.move.failed.conflict": "No se ha movido: otra persona ha cambiado esta entrada antes. El árbol se ha recargado — míralo otra vez y muévelo entonces.",
|
|
72
|
+
"tree.move.failed.gone": "No se ha movido: esa carpeta ya no existe.",
|
|
73
|
+
"tree.move.failed.other": "No se ha podido mover. Comprueba tu acceso e inténtalo de nuevo.",
|
|
74
|
+
"resource.menu": "Más acciones para {title}",
|
|
75
|
+
"resource.rename": "Cambiar el nombre",
|
|
76
|
+
"resource.renameTitle": "Cambiar el nombre de {title}",
|
|
77
|
+
"resource.archive": "Archivar",
|
|
78
|
+
"resource.export": "Descargar como zip",
|
|
79
|
+
"tree.new.importZip": "Importar un paquete zip",
|
|
80
|
+
"tree.new.importFolder": "Importar una carpeta",
|
|
81
|
+
"tree.importFailed": "La importación no se ha aplicado. No se ha creado nada — comprueba el paquete e inténtalo de nuevo.",
|
|
82
|
+
"resource.exportFailed": "La exportación no se ha podido descargar. Comprueba tu acceso e inténtalo de nuevo.",
|
|
83
|
+
"archive.title": "Archivo",
|
|
84
|
+
"archive.description": "Todo lo que has archivado, lo más reciente primero. Al restaurar, una entrada vuelve al sitio donde estaba.",
|
|
85
|
+
"archive.restore": "Restaurar {title}",
|
|
86
|
+
"archive.restoreAction": "Restaurar",
|
|
87
|
+
"archive.empty": "No hay nada archivado.",
|
|
88
|
+
"archive.failed": "El archivo no se ha podido leer. Comprueba tu acceso e inténtalo de nuevo.",
|
|
89
|
+
"archive.restoreFailed": "No se ha restaurado. Puede que otra persona lo haya cambiado — recarga e inténtalo de nuevo.",
|
|
90
|
+
"archive.archivedAt": "Archivado {when}",
|
|
91
|
+
"resource.conflict": "No se ha cambiado: otra persona ha cambiado esta entrada antes. Recárgala e inténtalo de nuevo.",
|
|
92
|
+
"resource.forbidden": "No se ha cambiado: no puedes cambiar esta entrada.",
|
|
93
|
+
"resource.failed": "El cambio no se ha guardado. Carga la versión más reciente e inténtalo de nuevo.",
|
|
94
|
+
"node.empty": "Todavía no se ha añadido nada.",
|
|
95
|
+
"node.download": "Descargar el archivo",
|
|
96
|
+
"node.attachmentHelp": "El archivo canónico se guarda en privado en Intel. Su proyección legible por la IA se indexa por separado.",
|
|
97
|
+
"node.table.summary": "{rows} filas, {columns} columnas",
|
|
98
|
+
"node.table.download": "Descargar CSV",
|
|
99
|
+
"node.table.empty": "Todavía no hay filas. Las filas las añaden los flujos y los agentes.",
|
|
100
|
+
"node.table.undefined": "Esta tabla todavía no tiene columnas.",
|
|
101
|
+
"node.select": "Elige un documento o una carpeta para trabajar con ello.",
|
|
102
|
+
"node.folderEmpty": "En esta carpeta todavía no hay nada. Crea algo con el más de la barra lateral.",
|
|
103
|
+
"node.folderFailed": "Esta carpeta no se ha podido leer.",
|
|
104
|
+
"node.changed": "Cambiado",
|
|
105
|
+
"node.rowActions": "Acciones",
|
|
106
|
+
"node.kind.folder": "Carpeta",
|
|
107
|
+
"node.kind.document": "Documento",
|
|
108
|
+
"node.kind.attachment": "Subida",
|
|
109
|
+
"node.kind.table": "Tabla",
|
|
110
|
+
"node.kind.agent": "Agente",
|
|
111
|
+
"node.kind.flow": "Flujo",
|
|
112
|
+
"node.share": "Compartir",
|
|
113
|
+
"node.shareAction": "Conceder acceso",
|
|
114
|
+
"node.email": "Dirección de correo",
|
|
115
|
+
"node.verbs": "Qué permite este acceso",
|
|
116
|
+
"node.verb.read": "Leer",
|
|
117
|
+
"node.verb.write": "Escribir",
|
|
118
|
+
"node.verb.execute": "Ejecutar",
|
|
119
|
+
"node.verb.share": "Compartir",
|
|
120
|
+
"node.verbHint.read": "Ver todo lo que hay aquí dentro",
|
|
121
|
+
"node.verbHint.write": "Cambiar y añadir cosas",
|
|
122
|
+
"node.verbHint.execute": "Iniciar los flujos que hay aquí dentro",
|
|
123
|
+
"node.verbHint.execute.agent": "Usar este agente — su chat, sus horarios y su dirección MCP",
|
|
124
|
+
"node.verbHint.share": "Dar acceso a otras personas",
|
|
125
|
+
"node.organization": "Todas las personas de la organización",
|
|
126
|
+
"node.revokeShare": "Retirar el acceso",
|
|
127
|
+
"node.noGrants": "Fuera de quien lo posee, todavía no tiene acceso nadie.",
|
|
128
|
+
"node.shareFailed": "El acceso no se ha cambiado. Comprueba tu permiso e inténtalo de nuevo.",
|
|
129
|
+
"node.shareUnreadable": "Los flujos de esta carpeta leen documentos que este acceso no cubre: {titles}.",
|
|
130
|
+
"node.shareUnreadableMore": "{count} más también quedan fuera de alcance, y no puedes verlos.",
|
|
131
|
+
"node.shareUnreadableHidden": "{count} documentos que leen los flujos de esta carpeta quedan fuera de alcance con este acceso. No puedes verlos.",
|
|
132
|
+
"node.shareUnreadableHint": "No se bloquea nada. Una ejecución de esos flujos simplemente se detendrá en ese paso para esa persona.",
|
|
133
|
+
"node.versions": "Historial de versiones",
|
|
134
|
+
"node.version": "Versión {sequence}",
|
|
135
|
+
"view.showGraph": "Mostrar este nivel como grafo",
|
|
136
|
+
"view.showEditor": "Volver al editor",
|
|
137
|
+
"view.showRuns": "Mostrar las ejecuciones de este flujo",
|
|
138
|
+
"view.graph": "Grafo de relaciones",
|
|
139
|
+
"view.graphTruncated": "{omitted} más quedan fuera — esta vista dibuja como mucho {limit}.",
|
|
140
|
+
"view.graphEmpty": "Aquí no hay nada que puedas ver.",
|
|
141
|
+
"view.graphFailed": "El grafo no se ha podido cargar.",
|
|
142
|
+
"node.graphZoomIn": "Acercar",
|
|
143
|
+
"node.graphZoomOut": "Alejar",
|
|
144
|
+
"node.graphReset": "Ajustar el grafo",
|
|
145
|
+
"node.operationFailed": "El cambio no se ha guardado. Carga la versión más reciente e inténtalo de nuevo.",
|
|
146
|
+
"node.loadFailed": "Este elemento no se ha podido cargar. Comprueba tu acceso e inténtalo de nuevo.",
|
|
147
|
+
"node.downloadFailed": "El adjunto no se ha podido descargar. Comprueba tu acceso e inténtalo de nuevo.",
|
|
148
|
+
"node.links": "Enlaces y retroenlaces",
|
|
149
|
+
"node.noLinks": "Todavía no hay relaciones explícitas.",
|
|
150
|
+
"node.linksHelp": "Las relaciones se escriben en el texto. Enlaza un documento desde el editor con el comando /; esta lista muestra el resultado y quién apunta aquí.",
|
|
151
|
+
"node.linkOutgoing": "Este documento enlaza con él",
|
|
152
|
+
"node.linkIncoming": "Enlaza con este documento",
|
|
153
|
+
"node.link.insert": "Enlazar un documento",
|
|
154
|
+
"node.link.group": "Documentos",
|
|
155
|
+
"node.link.search": "Buscar entre los documentos que puedes ver",
|
|
156
|
+
"node.link.noMatches": "Ningún documento tuyo coincide.",
|
|
157
|
+
"node.link.unresolved": "Documento no disponible",
|
|
158
|
+
"node.saveConflict": "Este documento ha cambiado en otro sitio. Recárgalo antes de volver a guardar.",
|
|
159
|
+
"node.saveError": "Este documento no se ha podido guardar. Recarga e inténtalo de nuevo.",
|
|
160
|
+
"tools.signingIn": "Iniciando tu sesión en el portal de la empresa…",
|
|
161
|
+
"tools.noAccess": "Sin acceso al portal de la empresa",
|
|
162
|
+
"tools.noAccessHelp": "Tu cuenta de Intel no llega al portal MCP de la empresa, así que aquí no hay nada que mostrar. Quién puede usar qué servidor lo decide la administración en el portal; pídeles que te incluyan.",
|
|
163
|
+
"tools.empty": "No tienes herramientas disponibles",
|
|
164
|
+
"tools.emptyHelp": "El portal ha respondido, y no ofrece ninguna herramienta a tu cuenta. Qué servidores existen y quién llega a ellos lo decide la administración en el portal.",
|
|
165
|
+
"tools.unreachable": "Portal no accesible",
|
|
166
|
+
"tools.unreachableHelp": "El portal MCP de la empresa no ha respondido, así que Intel no puede decir qué herramientas tienes. Aquí no se guarda nada; inténtalo otra vez cuando el portal responda.",
|
|
167
|
+
"tools.connectExpired": "El inicio de sesión en el portal ha tardado demasiado",
|
|
168
|
+
"tools.connectExpiredHelp": "La conexión con el portal MCP de la empresa tiene que completarse en unos minutos, y esta se ha agotado mientras la pantalla de autorización estaba abierta. No ha fallado nada ni se ha denegado nada — vuelve a empezar y confirma los servidores cuando aparezca la pantalla.",
|
|
169
|
+
"tools.connectRetry": "Empezar de nuevo",
|
|
170
|
+
"tools.connectFailed": "El inicio de sesión en el portal ha fallado",
|
|
171
|
+
"tools.connectFailedHelp": "El inicio de sesión en el portal MCP de la empresa se ha interrumpido antes de poder responderse. Es un fallo técnico, no una decisión sobre tu acceso — recarga para intentarlo otra vez y avisa a la administración si persiste.",
|
|
172
|
+
"tools.origin": "Origen",
|
|
173
|
+
"tools.originUnlisted": "No procede de un servidor listado",
|
|
174
|
+
"tools.originUnlistedHelp": "La lista de servidores del portal no nombra ningún servidor para estas. Aquí están sus propias herramientas de gestión — el portal no se lista a sí mismo — y también todo lo que venga de un servidor que la lista no mencionó.",
|
|
175
|
+
"tools.ungrouped": "Sin ordenar por servidor",
|
|
176
|
+
"tools.ungroupedHelp": "El portal MCP de la empresa no ha respondido de qué servidores vienen estas herramientas, así que Intel no puede decir qué herramienta pertenece a dónde. Abajo está todo lo que puedes alcanzar; solo falta la agrupación.",
|
|
177
|
+
"tools.inputSchema": "Esquema de entrada",
|
|
178
|
+
"tools.outputSchema": "Esquema del resultado",
|
|
179
|
+
"tools.destructiveShort": "Destructiva",
|
|
180
|
+
"tools.destructiveCount": "{count} destructivas",
|
|
181
|
+
"tools.toolCount": "{count} herramientas",
|
|
182
|
+
"tools.toolCountOne": "1 herramienta",
|
|
183
|
+
"tools.liveNote": "Esta lista es una consulta en vivo con tu propio acceso al portal. Intel no guarda ni las herramientas ni quién puede usarlas; ambas cosas las decide el portal.",
|
|
184
|
+
"tools.arguments": "Argumentos",
|
|
185
|
+
"tools.invalidJson": "Los argumentos tienen que ser un objeto JSON entre llaves; una lista o un valor suelto no sirven.",
|
|
186
|
+
"flows.select": "Elige un flujo o crea uno para abrir el editor visual.",
|
|
187
|
+
"flows.inspect": "Elige un paso para editar su configuración.",
|
|
188
|
+
"flows.publish": "Publicar",
|
|
189
|
+
"flows.publishNothing": "Nada nuevo que publicar",
|
|
190
|
+
"flows.publishNeeded": "Publicar — hasta entonces no puede empezar ninguna ejecución",
|
|
191
|
+
"flows.validate": "Comprobar si se ejecutaría",
|
|
192
|
+
"flows.validateReady": "Este flujo empezaría ahora mismo.",
|
|
193
|
+
"flows.validateWhen": "Comprobado {when}. El acceso a las herramientas se pide cada vez con tu propio token, así que esta respuesta es una instantánea.",
|
|
194
|
+
"flows.problem.flow_execute_forbidden": "No puedes ejecutar este flujo",
|
|
195
|
+
"flows.problem.flow_not_published": "El flujo no está publicado",
|
|
196
|
+
"flows.problem.flow_graph_invalid": "El grafo no se puede ejecutar",
|
|
197
|
+
"flows.problem.flow_tools_unavailable": "Una herramienta está fuera de alcance",
|
|
198
|
+
"flows.problem.flow_node_forbidden": "Material que lee un paso está fuera de alcance",
|
|
199
|
+
"flows.problem.flow_subflow_unavailable": "Un flujo llamado no está disponible",
|
|
200
|
+
"flows.problem.flow_subflow_not_published": "Un flujo llamado no está publicado",
|
|
201
|
+
"flows.addNode": "Añadir un paso",
|
|
202
|
+
"flows.closePalette": "Cerrar la barra de pasos",
|
|
203
|
+
"flows.nodePalette": "Tipos de paso",
|
|
204
|
+
"flows.node.trigger": "Inicio",
|
|
205
|
+
"flows.startExists": "Un flujo tiene exactamente un inicio, y este ya lo tiene.",
|
|
206
|
+
"flows.node.instruction": "Instrucción",
|
|
207
|
+
"flows.node.folder": "Carpeta",
|
|
208
|
+
"flows.node.document": "Documento",
|
|
209
|
+
"flows.node.upload": "Subida",
|
|
210
|
+
"flows.node.table": "Tabla",
|
|
211
|
+
"flows.node.tool": "Herramienta",
|
|
212
|
+
"flows.node.condition": "Condición",
|
|
213
|
+
"flows.node.subflow": "Flujo",
|
|
214
|
+
"flows.node.output": "Fin",
|
|
215
|
+
"flows.instruction": "Instrucción para la IA o la persona",
|
|
216
|
+
"flows.instructionHint": "Las condiciones, comprobaciones y bifurcaciones se pueden describir aquí con palabras. No hace falta un paso propio para cada decisión pequeña.",
|
|
217
|
+
"flows.linkEmpty": "Todavía no se ha elegido nada",
|
|
218
|
+
"picker.search": "Buscar por nombre",
|
|
219
|
+
"picker.top": "Todas las carpetas",
|
|
220
|
+
"picker.open": "Abrir {title}",
|
|
221
|
+
"picker.nothing": "Aquí no hay nada",
|
|
222
|
+
"flows.layer.marker": "Marcas",
|
|
223
|
+
"flows.layer.step": "Pasos",
|
|
224
|
+
"flows.layer.link": "Conexiones",
|
|
225
|
+
"flows.subflowTarget": "Flujo al que llamar",
|
|
226
|
+
"flows.selectSubflow": "Elige un flujo al que llamar",
|
|
227
|
+
"flows.subflowRule": "Un flujo puede llamar a un flujo de su propia carpeta o por debajo de ella, o a uno de una carpeta que todo el mundo puede ejecutar. Al publicar se nombra el motivo si no puede.",
|
|
228
|
+
"flows.subflowVersion": "Versión a la que llamar",
|
|
229
|
+
"flows.subflowVersion.latest": "La más reciente — se congela al publicar este flujo",
|
|
230
|
+
"flows.subflowVersion.follows": "Siempre la más reciente — esta llamada va con ella",
|
|
231
|
+
"flows.subflowVersion.pinned": "Versión congelada",
|
|
232
|
+
"flows.subflowVersionHint.latest": "Al publicar, esto se sustituye por la versión que el flujo llamado tenga publicada entonces, así que un cambio posterior allí deja este flujo en paz.",
|
|
233
|
+
"flows.subflowVersionHint.follows": "Esta llamada toma lo que el flujo llamado tenga publicado en el momento de ejecutarse. Un cambio allí cambia también este flujo.",
|
|
234
|
+
"flows.subflowVersionHint.pinned": "Esta llamada sigue ejecutando la versión a la que se congeló. Vuelve a ponerla en «la más reciente» para recoger la próxima publicación del flujo llamado.",
|
|
235
|
+
"flows.subflowBadge.follows": "va con ella",
|
|
236
|
+
"flows.subflowBadge.pinned": "congelada",
|
|
237
|
+
"flows.publishPreview": "Antes de publicar",
|
|
238
|
+
"flows.publishPreviewIntro": "Al publicar se congela cada llamada marcada abajo a la versión que nombra. Una llamada puesta en «siempre la más reciente» se queda como está.",
|
|
239
|
+
"flows.publishPreviewEmpty": "Este flujo no llama a ningún otro flujo, así que al publicar no se congela nada.",
|
|
240
|
+
"flows.publishPreviewFreeze": "se congela a la versión {sequence}",
|
|
241
|
+
"flows.publishPreviewPinned": "ya congelada a la versión {sequence}",
|
|
242
|
+
"flows.publishPreviewFollows": "va con el flujo llamado",
|
|
243
|
+
"flows.publishPreviewUnavailable": "El flujo llamado todavía no ha publicado nada, así que esto no se puede congelar.",
|
|
244
|
+
"flows.publishConfirm": "Publicar",
|
|
245
|
+
"flows.publishFailed": "La publicación ha fallado. Carga la versión más reciente e inténtalo de nuevo.",
|
|
246
|
+
"flows.tool": "Herramienta MCP",
|
|
247
|
+
"flows.selectTool": "Elige una herramienta descubierta",
|
|
248
|
+
"flows.operationFailed": "La operación del flujo ha fallado. Carga la versión más reciente e inténtalo de nuevo.",
|
|
249
|
+
"flows.needs": "Qué necesita este flujo",
|
|
250
|
+
"flows.needsHint": "Leído del grafo. Si una persona concreta llega a algo de esto se decide cuando lo ejecuta.",
|
|
251
|
+
"flows.needsNodes": "Documentos",
|
|
252
|
+
"flows.needsTools": "Herramientas",
|
|
253
|
+
"flows.needsHidden": "{count} más que no puedes ver",
|
|
254
|
+
"flows.needsEmpty": "Este flujo no lee ningún documento ni llama a ninguna herramienta.",
|
|
255
|
+
"flows.needsFailed": "Lo que necesita este flujo no se ha podido cargar.",
|
|
256
|
+
"runs.title": "Ejecuciones",
|
|
257
|
+
"runs.onlyFailed": "Solo las ejecuciones fallidas",
|
|
258
|
+
"runs.empty": "Este flujo todavía no se ha ejecutado.",
|
|
259
|
+
"runs.emptyFailed": "Ninguna ejecución de este flujo ha fallado.",
|
|
260
|
+
"runs.more": "Mostrar ejecuciones anteriores",
|
|
261
|
+
"runs.stillRunning": "todavía en marcha",
|
|
262
|
+
"runs.failedAt": "Ha fallado en {step}:",
|
|
263
|
+
"runs.failedToLoad": "Las ejecuciones de este flujo no se han podido cargar.",
|
|
264
|
+
"runs.stepsFailedToLoad": "Los pasos de esta ejecución no se han podido cargar.",
|
|
265
|
+
"runs.stepsEmpty": "Esta ejecución todavía no ha completado ningún paso.",
|
|
266
|
+
"runs.stepsTitle": "Pasos",
|
|
267
|
+
"runs.durationMs": "{value} ms",
|
|
268
|
+
"runs.durationSeconds": "{value} s",
|
|
269
|
+
"runs.durationMinutes": "{value} min",
|
|
270
|
+
"runs.status.queued": "En cola",
|
|
271
|
+
"runs.status.running": "En marcha",
|
|
272
|
+
"runs.status.waiting": "Esperando",
|
|
273
|
+
"runs.status.completed": "Completada",
|
|
274
|
+
"runs.status.failed": "Fallida",
|
|
275
|
+
"runs.status.cancelled": "Cancelada",
|
|
276
|
+
"runs.trigger.manual": "iniciada a mano",
|
|
277
|
+
"runs.trigger.subflow": "llamada por otro flujo",
|
|
278
|
+
"runs.outcome.completed": "hecha",
|
|
279
|
+
"runs.outcome.failed": "fallida",
|
|
280
|
+
"agent.tabs": "Vistas del agente",
|
|
281
|
+
"agent.tab.profile": "Perfil",
|
|
282
|
+
"agent.tab.chat": "Chat",
|
|
283
|
+
"agent.tab.calendar": "Calendario",
|
|
284
|
+
"agent.tab.log": "Registro",
|
|
285
|
+
"agent.statusRunning": "En marcha",
|
|
286
|
+
"agent.statusIdle": "Inactivo",
|
|
287
|
+
"agent.statusPaused": "En pausa",
|
|
288
|
+
"agent.statusUnknown": "Entorno de ejecución no accesible",
|
|
289
|
+
"agent.nextRun": "próxima ejecución {when} ({zone})",
|
|
290
|
+
"agent.noNextRun": "sin ejecución programada",
|
|
291
|
+
"agent.pausedNoRuns": "no se dispara ningún horario, no se acepta ninguna tarea ni ningún correo; el chat sigue respondiendo",
|
|
292
|
+
"agent.pause": "Pausar",
|
|
293
|
+
"agent.resume": "Reanudar",
|
|
294
|
+
"agent.runNow": "Ejecutar ahora",
|
|
295
|
+
"agent.runNowNoTarget": "Este agente no tiene horario, así que no hay nada que ejecutar.",
|
|
296
|
+
"agent.runNowPaused": "Reanuda el agente antes de ejecutarlo.",
|
|
297
|
+
"agent.actionFailed": "Eso no ha funcionado. Inténtalo de nuevo.",
|
|
298
|
+
"agent.keyMissing": "Este agente no tiene clave de aplicación, así que no puede empezar ninguna ejecución. Sustituye su clave en «Identidad», dentro del perfil.",
|
|
299
|
+
"agent.notPermitted": "Puedes leer este agente, pero no manejarlo. Manejar un agente necesita el permiso «ejecutar agentes».",
|
|
300
|
+
"agent.noRuntime": "Esta instalación no tiene entorno de ejecución de agentes, así que este agente no puede chatear ni ejecutarse aquí. Su definición se conserva y vuelve a funcionar en una instalación que sí lo tenga.",
|
|
301
|
+
"agent.loadFailed": "Este agente no se ha podido cargar. Comprueba tu acceso e inténtalo de nuevo.",
|
|
302
|
+
"agent.saveFailed": "El cambio no se ha guardado. Puede que otra persona haya cambiado este agente antes — recarga e inténtalo de nuevo.",
|
|
303
|
+
"agent.noDefinition": "Este agente todavía no tiene definición, así que aquí no hay nada que cambiar.",
|
|
304
|
+
"agent.identity": "Identidad",
|
|
305
|
+
"agent.identityHint": "La aplicación de Gate con la que actúa este agente. Sus permisos en el árbol deciden qué puede leer y escribir una ejecución, y su clave vive cifrada en el entorno de ejecución de agentes — nunca aquí y nunca en la definición.",
|
|
306
|
+
"agent.principal": "Aplicación de Gate",
|
|
307
|
+
"agent.principalNone": "Ninguna",
|
|
308
|
+
"agent.principalNoneHint": "Este agente no tiene aplicación de Gate, así que no hay ninguna clave que sustituir. Los agentes creados antes de esta función, importados o restaurados desde un paquete reciben una en Gate.",
|
|
309
|
+
"agent.rotateKey": "Sustituir la clave",
|
|
310
|
+
"agent.rotateKeyConfirm": "Se emite una clave nueva y se entrega al entorno de ejecución de agentes. La clave anterior deja de funcionar de inmediato, y aquí nunca se muestra ninguna clave.",
|
|
311
|
+
"agent.rotateKeyDone": "La clave se ha sustituido. El agente funciona con la nueva a partir de su próxima ejecución.",
|
|
312
|
+
"agent.rotateKeyFailed": "La clave no se ha sustituido. Inténtalo otra vez — e insiste hasta que salga bien: si Gate ya ha emitido la clave nueva, este agente no puede ejecutarse hasta que el entorno de ejecución la tenga.",
|
|
313
|
+
"agent.contact": "Contacto",
|
|
314
|
+
"agent.contactHint": "Cómo se llega a este agente. El chat integrado forma parte del entorno de ejecución y está siempre ahí.",
|
|
315
|
+
"agent.contactChat": "Chat integrado",
|
|
316
|
+
"agent.contactChatBuiltIn": "Integrado, siempre ahí",
|
|
317
|
+
"agent.contactMcp": "Dirección MCP",
|
|
318
|
+
"agent.copy": "Copiar",
|
|
319
|
+
"agent.copied": "Copiado",
|
|
320
|
+
"agent.contactAdd": "Añadir un contacto",
|
|
321
|
+
"agent.contactAddReason": "Una dirección de correo pertenece a la instalación, no a la definición del agente, así que se configura donde está desplegado el entorno de ejecución.",
|
|
322
|
+
"agent.knows": "Lo que sabe",
|
|
323
|
+
"agent.knowsHint": "Las carpetas que lee este agente y para qué cuenta cada una. Un mensaje de sistema lo instruye, el contexto semántico se busca, en la memoria se escribe de vuelta.",
|
|
324
|
+
"agent.addReference": "Añadir una carpeta",
|
|
325
|
+
"agent.removeReferenceOf": "Dejar de leer {title}",
|
|
326
|
+
"agent.pickFolder": "Carpeta que leer",
|
|
327
|
+
"agent.roleFor": "Para qué cuenta {title}",
|
|
328
|
+
"agent.role.system-message": "Mensaje de sistema",
|
|
329
|
+
"agent.role.semantic-context": "Contexto semántico",
|
|
330
|
+
"agent.role.memory": "Memoria",
|
|
331
|
+
"agent.unknownEntry": "No disponible para ti",
|
|
332
|
+
"agent.tools": "Herramientas",
|
|
333
|
+
"agent.toolsHint": "Los servidores MCP que le das a este agente, de entre los que tú mismo alcanzas. Intel guarda la elección, nunca el permiso — las herramientas que hay detrás de un servidor se leen en vivo con tu acceso al portal cada vez que el agente se ejecuta.",
|
|
334
|
+
"agent.addTool": "Añadir una herramienta",
|
|
335
|
+
"agent.removeToolServerOf": "Quitarle {name} a este agente",
|
|
336
|
+
"agent.toolServerUnavailable": "Ya no alcanzas este servidor",
|
|
337
|
+
"agent.toolsDelegationNotice": "Quien pueda ejecutar este agente actúa sobre tu conexión.",
|
|
338
|
+
"agent.toolsNotConnected": "Todavía no has iniciado sesión en el portal de la empresa, así que no hay nada que ceder. Abre «Herramientas» para conectarte.",
|
|
339
|
+
"agent.toolsNoneLeft": "Todos los servidores que alcanzas ya están dados a este agente.",
|
|
340
|
+
"agent.schedule": "Horario",
|
|
341
|
+
"agent.scheduleHint": "Cuándo actúa este agente por su cuenta y qué ejecuta. Cada horario conserva la zona horaria con la que se creó, así que significa la misma hora para todo el que lo lea.",
|
|
342
|
+
"agent.scheduleEmpty": "Este agente solo se ejecuta cuando se le pide.",
|
|
343
|
+
"agent.addSchedule": "Añadir un horario",
|
|
344
|
+
"agent.removeSchedule": "Quitar este horario",
|
|
345
|
+
"agent.scheduleTarget": "Qué ejecutar",
|
|
346
|
+
"agent.cron": "Cuándo",
|
|
347
|
+
"agent.scheduleTimezone": "Zona horaria",
|
|
348
|
+
"agent.scheduleTimezoneHint": "En qué se lee la expresión de arriba. Se propone a partir de tu propia preferencia y se guarda con el horario — si cambias tu preferencia más adelante, este se queda como está.",
|
|
349
|
+
"agent.cronHint": "Cinco campos: minuto, hora, día del mes, mes, día de la semana. «0 8 * * *» es todos los días a las 08:00 en la zona horaria de abajo.",
|
|
350
|
+
"agent.model": "Modelo",
|
|
351
|
+
"agent.modelHint": "Qué modelo piensa. El entorno de ejecución recoge un cambio en menos de un minuto.",
|
|
352
|
+
"agent.modelFigures": "{context} de contexto · {input} $ de entrada / {output} $ de salida por cada millón de tokens",
|
|
353
|
+
"agent.calendar": "Próximas ejecuciones",
|
|
354
|
+
"agent.calendarZones": "Cada hora se muestra en la zona horaria de su propio horario, porque es cuando se ejecuta el agente.",
|
|
355
|
+
"agent.calendarUnreadable": "Ninguno de los horarios de este agente se puede leer como expresión cron, así que no hay nada planificado.",
|
|
356
|
+
"agent.log": "Ejecuciones",
|
|
357
|
+
"agent.logEmpty": "Este agente todavía no se ha ejecutado.",
|
|
358
|
+
"agent.logFailed": "Las ejecuciones no se han podido leer. Puede que el entorno de ejecución de agentes no sea accesible desde aquí.",
|
|
359
|
+
"agent.runFailed": "Esta ejecución no se ha podido leer.",
|
|
360
|
+
"agent.runLog": "Lo que ha hecho esta ejecución",
|
|
361
|
+
"agent.runLogEmpty": "Esta ejecución no ha registrado nada.",
|
|
362
|
+
"agent.steps": "{count} pasos",
|
|
363
|
+
"agent.trigger.chat": "desde el chat",
|
|
364
|
+
"agent.trigger.schedule": "por horario",
|
|
365
|
+
"agent.trigger.mail": "desde un correo",
|
|
366
|
+
"agent.trigger.task": "pedido por MCP",
|
|
367
|
+
"agent.trigger.manual": "iniciada a mano",
|
|
368
|
+
"agent.chatEmpty": "Escríbele a este agente. Responde con lo que sabe y con las herramientas que puede alcanzar.",
|
|
369
|
+
"agent.chatInput": "Mensaje para este agente",
|
|
370
|
+
"agent.chatPlaceholder": "Pregunta algo…",
|
|
371
|
+
"agent.chatSend": "Enviar",
|
|
372
|
+
"agent.chatScrollDown": "Saltar al mensaje más reciente",
|
|
373
|
+
"settings.title": "Preferencias",
|
|
374
|
+
"settings.description": "Idioma, apariencia y zona horaria. Las tres se aplican de inmediato y se recuerdan en este dispositivo.",
|
|
375
|
+
"settings.language": "Idioma",
|
|
376
|
+
"settings.appearance": "Apariencia",
|
|
377
|
+
"settings.appearance.system": "Seguir al sistema",
|
|
378
|
+
"settings.appearance.light": "Claro",
|
|
379
|
+
"settings.appearance.dark": "Oscuro",
|
|
380
|
+
"settings.timezone": "Zona horaria",
|
|
381
|
+
"settings.timezoneHint": "Se usa para leer las horas y se propone cuando le das un horario a un agente. Un horario conserva la zona con la que se creó, así que viajar no mueve las ejecuciones que ya has fijado.",
|
|
382
|
+
"auth.signOut": "Cerrar sesión",
|
|
383
|
+
"auth.signOutFailed": "El cierre de sesión ha fallado. Comprueba tu conexión e inténtalo de nuevo.",
|
|
384
|
+
"signIn.refusedTitle": "Sesión iniciada, pero no aceptada",
|
|
385
|
+
"signIn.refusedBody": "Gate ha iniciado tu sesión, y esta instalación no ha aceptado la credencial. No se te ha devuelto al inicio de sesión, porque ahí es donde empieza el bucle.",
|
|
386
|
+
"signIn.refusedLikely": "Lo más habitual es que la sesión caducara entre los dos pasos, o que la credencial se emitiera para otra instalación. Recargar merece un intento; después de eso, tiene que mirarlo quien gestiona esta instalación.",
|
|
387
|
+
"signIn.refusedRetry": "Intentarlo de nuevo",
|
|
388
|
+
"common.title": "Título",
|
|
389
|
+
"common.create": "Crear",
|
|
390
|
+
"common.save": "Guardar",
|
|
391
|
+
"common.saving": "Guardando…",
|
|
392
|
+
"common.saved": "Guardado",
|
|
393
|
+
"common.saveDirty": "Guardar los cambios sin guardar",
|
|
394
|
+
"common.unsavedTitle": "Cambios sin guardar",
|
|
395
|
+
"common.unsavedBody": "Esto tiene cambios que no se han guardado. Si sales ahora, se descartan.",
|
|
396
|
+
"common.unsavedStay": "Quedarse y guardar",
|
|
397
|
+
"common.unsavedLeave": "Salir sin guardar",
|
|
398
|
+
"common.loading": "Cargando…",
|
|
399
|
+
"common.retry": "Intentarlo de nuevo",
|
|
400
|
+
"common.close": "Cerrar",
|
|
401
|
+
"common.unavailable": "Intel no está disponible en este momento."
|
|
402
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createContext, type ReactNode, useContext, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { createI18n } from "./i18n.ts";
|
|
3
|
+
import type { I18n, LanguageChoice, Languages } from "./i18n.types.ts";
|
|
4
|
+
|
|
5
|
+
// The React seam of the i18n. Until #227 the instance travelled in the router context, which is
|
|
6
|
+
// static — there was no way to switch language without reloading the page. It is a context now, for
|
|
7
|
+
// the same reason the data provider is one: it is a dependency, not a module singleton. Unlike the
|
|
8
|
+
// data provider it has a sensible default — the English instance — so a component rendered without
|
|
9
|
+
// a provider (component tests) does not throw but simply speaks English.
|
|
10
|
+
const defaultI18n = createI18n();
|
|
11
|
+
|
|
12
|
+
const I18nContext = createContext<I18n>(defaultI18n);
|
|
13
|
+
|
|
14
|
+
// The choice between the available languages. `null` without a provider — not an empty list: the
|
|
15
|
+
// settings dialog asks precisely whether there is anything to switch at all.
|
|
16
|
+
const LanguageContext = createContext<LanguageChoice | null>(null);
|
|
17
|
+
|
|
18
|
+
interface I18nProviderProps {
|
|
19
|
+
languages: Languages;
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The provider holds the active language — the only state the i18n has at runtime. It is client
|
|
24
|
+
// state (a preference), so it lives locally; remembering it is the job of createLanguages.
|
|
25
|
+
export function I18nProvider({ languages, children }: I18nProviderProps) {
|
|
26
|
+
const [current, setCurrent] = useState(languages.initial);
|
|
27
|
+
const i18n = useMemo(() => languages.i18n(current), [languages, current]);
|
|
28
|
+
|
|
29
|
+
// The root document reports the same language as the active catalog — at start AND after every
|
|
30
|
+
// switch. Otherwise a screen reader reads Spanish text with English pronunciation rules, and the
|
|
31
|
+
// browser offers a translation that is none. index.html only carries the static default.
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
document.documentElement.lang = i18n.locale;
|
|
34
|
+
}, [i18n.locale]);
|
|
35
|
+
|
|
36
|
+
const choice = useMemo<LanguageChoice>(
|
|
37
|
+
() => ({
|
|
38
|
+
codes: languages.codes,
|
|
39
|
+
current,
|
|
40
|
+
select: (code) => {
|
|
41
|
+
languages.remember(code);
|
|
42
|
+
setCurrent(code);
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
[languages, current],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<I18nContext value={i18n}>
|
|
50
|
+
<LanguageContext value={choice}>{children}</LanguageContext>
|
|
51
|
+
</I18nContext>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The whole instance, not just `t`: intel's screens hand `i18n` down to presentational children as a
|
|
56
|
+
// prop (ToolRows, GraphNotice, RelationGraphView …), and those need the object. A `useT()` would
|
|
57
|
+
// force either drilling `t` separately or turning every one of those into a hook consumer — a
|
|
58
|
+
// different refactor, and one that buys nothing.
|
|
59
|
+
export function useI18n(): I18n {
|
|
60
|
+
return useContext(I18nContext);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function useLanguage(): LanguageChoice | null {
|
|
64
|
+
return useContext(LanguageContext);
|
|
65
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import custom from "../custom.json";
|
|
3
|
+
import de from "../de.json";
|
|
4
|
+
import en from "../en.json";
|
|
5
|
+
import es from "../es.json";
|
|
6
|
+
import { CatalogSchema, createI18n } from "../i18n.ts";
|
|
7
|
+
import type { Catalog, Languages, LanguagesDeps } from "../i18n.types.ts";
|
|
8
|
+
|
|
9
|
+
// The language choice: which languages exist is the built-in set plus whatever `intel build` wrote
|
|
10
|
+
// into custom.json; which one runs is the reader's decision. Kept apart from createI18n, because the
|
|
11
|
+
// lookup needs to know none of this — it receives a catalog and reads from it.
|
|
12
|
+
|
|
13
|
+
// localStorage is right here and ONLY here: the language choice is a UI preference, not a mirror of
|
|
14
|
+
// server data (TanStack Query holds that) and not a secret. That is exactly why it survives a
|
|
15
|
+
// reload.
|
|
16
|
+
const STORE_KEY = "intel.language";
|
|
17
|
+
|
|
18
|
+
// The catalogs shipped with the UI. English is the default AND the key source; German and Spanish
|
|
19
|
+
// are translations of it.
|
|
20
|
+
//
|
|
21
|
+
// ⚠️ They cross no build step that could check them: `intel build` validates the CUSTOMER's
|
|
22
|
+
// language files against en.json, and these three are already in the package. What replaces that
|
|
23
|
+
// check is `i18n.unit.ts`, which compares the key sets of every file next to en.json. Without it a
|
|
24
|
+
// key added only to en.json would be English text in two other languages, and only a reader using
|
|
25
|
+
// those languages would ever notice.
|
|
26
|
+
const BUILT_IN: Record<string, Catalog> = {
|
|
27
|
+
en: CatalogSchema.parse(en) as Catalog,
|
|
28
|
+
de: CatalogSchema.parse(de) as Catalog,
|
|
29
|
+
es: CatalogSchema.parse(es) as Catalog,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// The language everything falls back to: it is compiled in and can therefore never be missing.
|
|
33
|
+
const FALLBACK = "en";
|
|
34
|
+
|
|
35
|
+
// custom.json is a build artifact — `intel build` writes it and validates each customer file
|
|
36
|
+
// strictly against en.json. It is still parsed here, because a custom.json from before the switcher
|
|
37
|
+
// is a FLAT catalog: accepting that silently would swap the customer's translation for English
|
|
38
|
+
// without a word.
|
|
39
|
+
const BundleSchema = z.strictObject({
|
|
40
|
+
defaultLanguage: z.string().min(1),
|
|
41
|
+
catalogs: z.record(z.string(), CatalogSchema),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export function createLanguages({ store }: LanguagesDeps = {}): Languages {
|
|
45
|
+
const bundle = BundleSchema.parse(custom);
|
|
46
|
+
// The built-in catalogs always stand available; the bundle comes last so a customer catalog under
|
|
47
|
+
// the same code REPLACES the shipped one rather than being ignored. Without that, changing one
|
|
48
|
+
// sentence of the German UI would mean maintaining a fourth language.
|
|
49
|
+
const catalogs: Record<string, Catalog> = { ...BUILT_IN, ...bundle.catalogs };
|
|
50
|
+
const codes = Object.keys(catalogs);
|
|
51
|
+
|
|
52
|
+
// A remembered language that no longer exists (the customer removed it from intel.json) is
|
|
53
|
+
// discarded rather than running half the UI on the English fallback.
|
|
54
|
+
const remembered = store?.getItem(STORE_KEY);
|
|
55
|
+
const preferred =
|
|
56
|
+
remembered !== null && remembered !== undefined && codes.includes(remembered)
|
|
57
|
+
? remembered
|
|
58
|
+
: bundle.defaultLanguage;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
codes,
|
|
62
|
+
initial: codes.includes(preferred) ? preferred : FALLBACK,
|
|
63
|
+
// An unknown code yields the English instance rather than an empty catalog: `t` would otherwise
|
|
64
|
+
// return every key verbatim, which looks like a broken screen instead of an untranslated one.
|
|
65
|
+
i18n: (code) => {
|
|
66
|
+
const catalog = catalogs[code];
|
|
67
|
+
return catalog ? createI18n({ catalog }) : createI18n();
|
|
68
|
+
},
|
|
69
|
+
remember: (code) => store?.setItem(STORE_KEY, code),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// A language's name in its own language ("Deutsch", never "German") — that way everybody finds their
|
|
74
|
+
// entry even while the UI is in a language they cannot read. It therefore lives in no catalog: a
|
|
75
|
+
// language the customer adds tomorrow would otherwise need a new key in every language file. A code
|
|
76
|
+
// Intl does not know is shown unchanged.
|
|
77
|
+
export function languageName(code: string): string {
|
|
78
|
+
try {
|
|
79
|
+
return new Intl.DisplayNames([code], { type: "language" }).of(code) ?? code;
|
|
80
|
+
} catch {
|
|
81
|
+
return code;
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/i18n/i18n.ts
CHANGED
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import custom from "./custom.json";
|
|
3
2
|
import english from "./en.json";
|
|
4
|
-
import type { Catalog, I18n } from "./i18n.types.ts";
|
|
3
|
+
import type { Catalog, I18n, I18nDeps } from "./i18n.types.ts";
|
|
4
|
+
|
|
5
|
+
// The whole i18n of intel-ui. No framework: i18next and friends bring bundle weight and an
|
|
6
|
+
// abstraction (namespaces, plural rules, loading states) that has no consumer here. What is needed
|
|
7
|
+
// is a lookup plus placeholders — the twenty lines below.
|
|
8
|
+
//
|
|
9
|
+
// English is the default AND the source of keys: en.json is the only file in which a new string is
|
|
10
|
+
// born. A translated catalog arrives as `catalog`; whatever is missing from it falls back to the
|
|
11
|
+
// English text — an incomplete translation shows English words, never an empty screen or a raw key.
|
|
12
|
+
export const CatalogSchema = z.record(z.string(), z.string());
|
|
5
13
|
|
|
6
|
-
const CatalogSchema = z.record(z.string(), z.string());
|
|
7
|
-
const BundleSchema = z.strictObject({
|
|
8
|
-
defaultLanguage: z.string().min(1),
|
|
9
|
-
catalogs: z.record(z.string(), CatalogSchema),
|
|
10
|
-
});
|
|
11
14
|
const fallback = CatalogSchema.parse(english) as Catalog;
|
|
12
|
-
const bundle = BundleSchema.parse(custom);
|
|
13
15
|
|
|
14
|
-
export function createI18n(
|
|
15
|
-
const customCatalog = bundle.catalogs[language];
|
|
16
|
-
const catalog: Catalog = customCatalog ? { ...fallback, ...customCatalog } : fallback;
|
|
16
|
+
export function createI18n({ catalog = fallback }: I18nDeps = {}): I18n {
|
|
17
17
|
return {
|
|
18
18
|
locale: catalog.$locale ?? fallback.$locale ?? "en-US",
|
|
19
19
|
t(key, variables) {
|
|
20
|
+
// The key itself is the last fallback: a typo becomes visible on screen instead of vanishing
|
|
21
|
+
// as an empty string.
|
|
20
22
|
const template = catalog[key] ?? fallback[key] ?? key;
|
|
21
23
|
if (!variables) return template;
|
|
24
|
+
// Only known placeholders are replaced; an unfilled `{foo}` stays put — a visible gap beats a
|
|
25
|
+
// silent blank.
|
|
22
26
|
return template.replace(/\{(\w+)\}/g, (match, name: string) => {
|
|
23
27
|
const value = variables[name];
|
|
24
28
|
return value === undefined ? match : String(value);
|