# Galappxy — Propuesta Técnica: Sistema de Personalización de Conceptos e Idiomas

> **v1.0 · Febrero 2026**
> Propuesta de arquitectura para i18n + concept mapping multi-tenant

---

## Resumen ejecutivo

Este documento describe la arquitectura para un sistema que permite:

1. **Multi-idioma** — La interfaz completa se traduce a cualquier idioma (ES, EN, PT, etc.)
2. **Conceptos por vertical** — Cada marca usa terminología de su sector (alumno, paciente, miembro)
3. **Override por tenant** — Cada cliente individual puede personalizar cualquier concepto
4. **Documentación dinámica** — Guías y FAQs se adaptan automáticamente a los conceptos del tenant

Todo funciona con un sistema de 3 capas donde cada capa sobrescribe a la anterior, similar a cómo ya funciona el CSS dinámico del framework.

---

## 1. Arquitectura de 3 capas

```
┌─────────────────────────────────────────────────────────────┐
│                    PRIORIDAD DE RESOLUCIÓN                   │
│                                                             │
│  Capa 1: IDIOMA BASE                     "cliente"          │
│  (es.json, en.json, pt.json)             "client"           │
│  Textos genéricos del sistema            "cliente"          │
│                                                             │
│  Capa 2: MARCA / VERTICAL               "alumno"           │
│  (schoolibox.json, medicare.json)        "student"          │
│  Sobrescribe conceptos del sector        "aluno"            │
│                                                             │
│  Capa 3: TENANT (cliente individual)     "socio"            │
│  (desde la BD, tabla BRAND)              "associate"        │
│  El cliente personaliza lo que quiera    "associado"        │
│                                                             │
│  ─────────────────────────────────────────────────────────  │
│  RESULTADO FINAL:    "socio"  (Capa 3 gana siempre)        │
└─────────────────────────────────────────────────────────────┘
```

### Ejemplo concreto

| Concepto | Idioma base (ES) | School iBox | Tenant override | Resultado |
|----------|-----------------|-------------|-----------------|-----------|
| client.singular | cliente | alumno | socio | **socio** |
| client.plural | clientes | alumnos | socios | **socios** |
| charge.singular | cobro | colegiatura | mensualidad | **mensualidad** |
| organization | empresa | escuela | instituto | **instituto** |
| staff | empleado | docente | — | **docente** |

---

## 2. Tipos de contenido traducible

### 2.1 Conceptos (sustantivos)

Son los nombres de las entidades del sistema. Cada concepto tiene **formas gramaticales**:

```json
{
  "client": {
    "singular": "alumno",
    "plural": "alumnos",
    "article": "el",
    "article_plural": "los"
  }
}
```

Las formas `article` y `article_plural` son necesarias para idiomas con género gramatical (español, portugués, francés). En inglés siempre es "the".

**Conceptos del sistema:**

| Key | Genérico (ES) | School iBox | GymFlow | MediCare |
|-----|--------------|-------------|---------|----------|
| client | cliente | alumno | miembro | paciente |
| charge | cobro | colegiatura | membresía | consulta |
| payment | pago | pago | pago | pago |
| plan | plan | plan de pagos | plan | plan de tratamiento |
| branch | sucursal | campus | sede | clínica |
| service | servicio | materia | clase | especialidad |
| appointment | cita | clase | reservación | cita |
| staff | empleado | docente | entrenador | médico |
| organization | empresa | escuela | gimnasio | clínica |
| product | producto | material | suplemento | medicamento |
| invoice | factura | factura | factura | factura |
| provider | proveedor | proveedor | proveedor | proveedor |

### 2.2 Strings (textos del sistema)

Son las oraciones completas de la interfaz. Pueden contener **variables** y **conceptos interpolados**:

```json
{
  "payments": {
    "title": "{{$charge.plural}}",
    "subtitle": "Gestiona {{$charge.article_plural}} {{$charge.plural}} de tu {{$organization.singular}}",
    "no_pending": "No hay {{$charge.plural}} pendientes",
    "confirm_delete": "¿Eliminar {{$charge.article}} {{$charge.singular}} #{{id}} de {{name}} por {{amount}}?"
  }
}
```

**Sintaxis de interpolación:**

| Patrón | Tipo | Ejemplo | Resultado |
|--------|------|---------|-----------|
| `{{variable}}` | Variable dinámica | `{{name}}` | María López |
| `{{$concept.form}}` | Concepto | `{{$client.plural}}` | alumnos |
| `{{$concept.singular}}` | Concepto singular | `{{$charge.singular}}` | colegiatura |
| `{{$concept.article}}` | Artículo del concepto | `{{$charge.article}}` | la |

### 2.3 Documentación

Las guías y FAQs también usan la misma interpolación:

```json
{
  "docs": {
    "guide_payments": "Cómo registrar una {{$charge.singular}}",
    "faq_pending": "¿Cómo veo mis {{$charge.plural}} pendientes?",
    "faq_contact": "¿Cómo contacto a mi {{$organization.singular}}?"
  }
}
```

---

## 3. Modelo de datos (Backend)

### 3.1 Tabla: `i18n_locales`

Idiomas base del sistema.

```sql
CREATE TABLE i18n_locales (
  id           SERIAL PRIMARY KEY,
  code         VARCHAR(5) NOT NULL UNIQUE,  -- 'es', 'en', 'pt'
  name         VARCHAR(50) NOT NULL,         -- 'Español', 'English'
  is_default   BOOLEAN DEFAULT FALSE,
  strings      JSONB NOT NULL,               -- Textos del sistema
  concepts     JSONB NOT NULL,               -- Conceptos genéricos
  created_at   TIMESTAMP DEFAULT NOW(),
  updated_at   TIMESTAMP DEFAULT NOW()
);
```

### 3.2 Tabla: `i18n_brand_overrides`

Overrides por marca/vertical y por idioma.

```sql
CREATE TABLE i18n_brand_overrides (
  id           SERIAL PRIMARY KEY,
  brand_id     VARCHAR(50) NOT NULL,         -- 'schoolibox', 'medicare'
  locale_code  VARCHAR(5) NOT NULL,          -- 'es', 'en'
  strings      JSONB DEFAULT '{}',           -- Overrides de strings
  concepts     JSONB DEFAULT '{}',           -- Overrides de conceptos
  created_at   TIMESTAMP DEFAULT NOW(),
  updated_at   TIMESTAMP DEFAULT NOW(),
  UNIQUE(brand_id, locale_code)
);
```

### 3.3 Tabla: `i18n_tenant_overrides`

Overrides por tenant individual.

```sql
CREATE TABLE i18n_tenant_overrides (
  id           SERIAL PRIMARY KEY,
  tenant_id    INTEGER NOT NULL REFERENCES companies(id),
  locale_code  VARCHAR(5) NOT NULL,
  strings      JSONB DEFAULT '{}',
  concepts     JSONB DEFAULT '{}',
  created_at   TIMESTAMP DEFAULT NOW(),
  updated_at   TIMESTAMP DEFAULT NOW(),
  UNIQUE(tenant_id, locale_code)
);
```

### 3.4 Integración con JWT

El JWT ya trae `tenant_id` y `theme_config_json`. Se agrega:

```json
{
  "tenant_id": "acme-school",
  "brand": "schoolibox",
  "locale": "es",
  "theme_config_json": { "..." },
  "i18n_overrides": {
    "concepts": {
      "client": {
        "singular": "socio",
        "plural": "socios"
      }
    },
    "strings": {}
  }
}
```

**Nota:** Solo se envían los overrides del tenant en el JWT, no todo el diccionario. El frontend ya tiene los idiomas base y las marcas como archivos estáticos.

---

## 4. API endpoints

### 4.1 Obtener diccionario completo (para cache del frontend)

```
GET /api/i18n/bundle?locale=es&brand=schoolibox
```

Respuesta: merge de las 3 capas (idioma + marca + tenant del JWT).

### 4.2 Obtener/actualizar overrides del tenant

```
GET  /api/i18n/tenant-overrides?locale=es
PUT  /api/i18n/tenant-overrides
```

```json
// PUT body
{
  "locale": "es",
  "concepts": {
    "client": { "singular": "socio", "plural": "socios" }
  },
  "strings": {
    "payments": { "title": "Cuotas" }
  }
}
```

### 4.3 Listar conceptos editables

```
GET /api/i18n/editable-concepts
```

Devuelve la lista de conceptos que el tenant puede editar, con sus valores actuales en cada capa.

---

## 5. Frontend: Motor i18n

### 5.1 Archivo: `js/i18n-manager.js`

Clase `I18nManager` disponible como `GX.i18n`:

```javascript
// Registro de locales (archivos estáticos)
GX.i18n.registerLocale('es', { strings: {...}, concepts: {...} });
GX.i18n.registerLocale('en', { strings: {...}, concepts: {...} });

// Registro de marcas (archivos estáticos)
GX.i18n.registerBrand('schoolibox', 'es', { concepts: {...}, strings: {...} });
GX.i18n.registerBrand('schoolibox', 'en', { concepts: {...} });

// Inicialización (en login)
GX.i18n.init({
  locale: 'es',
  brand: 'schoolibox',
  tenantOverrides: jwt.i18n_overrides  // Desde el JWT
});

// Uso en código
GX.i18n.t('payments.title');                    // "Colegiaturas"
GX.i18n.t('welcome', { name: 'María' });        // "Hola, María"
GX.i18n.concept('client');                       // "alumno"
GX.i18n.concept('client', 'plural');             // "alumnos"
GX.i18n.concept('client', 'singular', true);     // "Alumno" (capitalizado)
```

### 5.2 Atributos en el HTML

```html
<!-- Texto simple -->
<h1 data-i18n="payments.title">Cobros</h1>

<!-- Con variables -->
<p data-i18n="dashboard.welcome"
   data-i18n-params='{"name":"María"}'>Hola, María</p>

<!-- Placeholder -->
<input data-i18n-placeholder="clients.search_placeholder"
       placeholder="Buscar clientes...">

<!-- Title/tooltip -->
<button data-i18n-title="common.save" title="Guardar">💾</button>
```

### 5.3 Aplicar al DOM

```javascript
// Actualiza todos los elementos con data-i18n
GX.i18n.applyToDOM();

// Solo dentro de un contenedor
GX.i18n.applyToDOM(document.getElementById('payments-view'));
```

### 5.4 Evento de cambio

```javascript
document.addEventListener('gx:i18n-change', (e) => {
  console.log('Idioma:', e.detail.locale);
  console.log('Marca:', e.detail.brand);
});
```

---

## 6. Flujo en producción

```
┌──────────┐     ┌──────────────────┐     ┌──────────────────────────────────┐
│  Login   │────▶│  API Response    │────▶│  Frontend                        │
│          │     │                  │     │                                  │
│          │     │  JWT:            │     │  1. Los archivos es.js, en.js    │
│          │     │  - locale: "es"  │     │     ya están cargados (estáticos)│
│          │     │  - brand: "..."  │     │  2. schoolibox.js, gymflow.js    │
│          │     │  - i18n_overrides│     │     ya están cargados (estáticos)│
│          │     │    {concepts:{}} │     │  3. GX.i18n.init({              │
│          │     │                  │     │       locale: jwt.locale,        │
│          │     │                  │     │       brand: jwt.brand,          │
│          │     │                  │     │       tenantOverrides: jwt.i18n  │
│          │     │                  │     │     });                          │
│          │     │                  │     │  4. GX.i18n.applyToDOM();        │
│          │     │                  │     │  5. Render app con textos ok     │
└──────────┘     └──────────────────┘     └──────────────────────────────────┘
```

### Flujo de cambio de idioma (en runtime)

```javascript
async function changeLanguage(newLocale) {
  // 1. El archivo del idioma ya está cargado
  // 2. Re-inicializar con nuevo locale
  GX.i18n.init({
    locale: newLocale,
    brand: currentBrand,
    tenantOverrides: currentOverrides,
  });

  // 3. Re-aplicar al DOM
  GX.i18n.applyToDOM();

  // 4. Guardar preferencia
  localStorage.setItem('gx_locale', newLocale);
}
```

---

## 7. Documentación personalizable

### 7.1 Textos de las guías

Las guías usan la misma interpolación de conceptos:

```markdown
# Cómo registrar {{$charge.article}} {{$charge.singular}}

1. Ve a la sección de **{{$charge.plural}}** en el menú lateral
2. Haz clic en **Nuevo {{$charge.singular}}**
3. Selecciona {{$client.article}} **{{$client.singular}}** al que deseas cobrar
4. Ingresa el monto y la fecha de vencimiento
5. Haz clic en **Guardar**

## Preguntas frecuentes

### ¿Cómo envío un recordatorio a {{$client.article}} {{$client.singular}}?
En la lista de {{$charge.plural}}, haz clic en el ícono de campana...
```

**Resultado para School iBox:**
> Cómo registrar **la colegiatura** → Ve a la sección de **colegiaturas** → Selecciona **el alumno**...

**Resultado para MediCare:**
> Cómo registrar **la consulta** → Ve a la sección de **consultas** → Selecciona **el paciente**...

### 7.2 Imágenes por marca

Cada marca puede tener screenshots diferentes en las guías. Estructura:

```
docs/
├── es/
│   ├── guides/
│   │   ├── payments.md          ← Texto con conceptos interpolados
│   │   └── clients.md
│   └── faqs/
│       └── general.md
├── en/
│   ├── guides/
│   │   └── payments.md
│   └── faqs/
│       └── general.md
└── assets/
    ├── schoolibox/              ← Screenshots de School iBox
    │   ├── payments-list.png
    │   ├── payments-new.png
    │   └── clients-list.png
    ├── gymflow/                 ← Screenshots de GymFlow
    │   ├── payments-list.png
    │   └── ...
    └── _default/                ← Fallback genérico
        └── ...
```

### 7.3 Interpolación de imágenes

En el markdown, las imágenes también se seleccionan por marca:

```markdown
![Lista de {{$charge.plural}}]({{$brand_assets}}/payments-list.png)
```

Donde `{{$brand_assets}}` resuelve a `/docs/assets/schoolibox/` o la marca correspondiente.

### 7.4 Overrides de documentación por tenant

Un tenant puede reemplazar imágenes con screenshots de su propio sistema:

```sql
CREATE TABLE i18n_tenant_docs (
  id          SERIAL PRIMARY KEY,
  tenant_id   INTEGER NOT NULL REFERENCES companies(id),
  doc_key     VARCHAR(100) NOT NULL,  -- 'guides/payments'
  locale      VARCHAR(5) NOT NULL,
  body_md     TEXT,                    -- Override del markdown completo (opcional)
  assets      JSONB DEFAULT '{}',     -- Mapeo de imágenes custom
  created_at  TIMESTAMP DEFAULT NOW(),
  updated_at  TIMESTAMP DEFAULT NOW(),
  UNIQUE(tenant_id, doc_key, locale)
);
```

```json
// assets column example:
{
  "payments-list.png": "https://cdn.acme-school.com/docs/mis-cobros.png",
  "payments-new.png": "https://cdn.acme-school.com/docs/nuevo-cobro.png"
}
```

---

## 8. UI de personalización (admin del tenant)

El admin del tenant tiene una pantalla para editar conceptos:

```
Configuración → Personalizar conceptos
┌────────────────────────────────────────────────────┐
│  Personalizar conceptos                            │
│  Cambia cómo se llaman las cosas en tu sistema     │
│                                                    │
│  ┌────────────┬──────────┬───────────────────────┐ │
│  │ Concepto   │ Default  │ Tu personalización    │ │
│  ├────────────┼──────────┼───────────────────────┤ │
│  │ Cliente    │ alumno   │ [socio          ]     │ │
│  │ Clientes   │ alumnos  │ [socios         ]     │ │
│  │ Cobro      │ colegiat │ [mensualidad    ]     │ │
│  │ Cobros     │ colegiat │ [mensualidades  ]     │ │
│  │ Empresa    │ escuela  │ [               ]     │ │
│  │ Empleado   │ docente  │ [               ]     │ │
│  └────────────┴──────────┴───────────────────────┘ │
│                                                    │
│  [Vista previa]              [Guardar cambios]     │
└────────────────────────────────────────────────────┘
```

El preview muestra en tiempo real cómo se verán los textos en el sistema.

---

## 9. Estructura de archivos (frontend)

```
shell/
├── js/
│   ├── theme-manager.js
│   └── i18n-manager.js          ← Motor de i18n
├── i18n/
│   ├── locales/
│   │   ├── es.js                ← Español base
│   │   ├── en.js                ← English base
│   │   └── pt.js                ← Português (futuro)
│   └── brands/
│       ├── schoolibox.js        ← School iBox (ES + EN)
│       ├── gymflow.js           ← GymFlow (ES + EN)
│       └── medicare.js          ← MediCare (ES + EN)
├── demo-i18n.html               ← Demo interactivo
└── ...
```

---

## 10. Integración con el CSS dinámico

El sistema de i18n se integra con el theming existente. Al cambiar de perfil/tenant:

```javascript
async function switchProfile(newTenantId, newJWT) {
  // 1. Cambiar tema visual
  await GX.theme.init(newJWT.tenant_id, newJWT.theme_config_json);

  // 2. Cambiar idioma y conceptos
  GX.i18n.init({
    locale: newJWT.locale,
    brand: newJWT.brand,
    tenantOverrides: newJWT.i18n_overrides,
  });

  // 3. Aplicar ambos al DOM
  GX.i18n.applyToDOM();
  lucide.createIcons();
}
```

---

## 11. Consideraciones de implementación

### Performance

- Los archivos de locale y marca son estáticos y se cachean en el browser
- Solo los overrides del tenant viajan en el JWT (pocos KB)
- `applyToDOM()` usa `querySelectorAll` que es nativo y rápido
- No se hace fetch adicional después del login

### Agregar un nuevo idioma

1. Crear `i18n/locales/pt.js` con todos los strings y conceptos
2. Agregar traducciones de marca en cada `brands/*.js`
3. Agregar opción en el selector de idioma

### Agregar un nuevo concepto

1. Agregarlo en todos los archivos de `locales/*.js`
2. Agregarlo en los `brands/*.js` relevantes
3. Usar `{{$nuevo_concepto.singular}}` en los strings

### Agregar una nueva marca

1. Crear `i18n/brands/nueva-marca.js`
2. Registrar con `GX.i18n.registerBrand('nueva-marca', 'es', {...})`
3. Solo necesita los conceptos que difieren del genérico

### Fallback

Si falta una traducción, el sistema muestra `[key.name]` y logea un warning en consola. Esto facilita detectar traducciones faltantes durante desarrollo.

---

## 12. Roadmap

### Fase 1 (actual)
- [x] Motor i18n (`i18n-manager.js`)
- [x] Locales: Español, English
- [x] Brands: School iBox, GymFlow, MediCare
- [x] Demo interactivo con override en tiempo real
- [x] Propuesta técnica

### Fase 2
- [ ] Modelo de datos en PostgreSQL
- [ ] API endpoints (CRUD de overrides)
- [ ] Integración con JWT existente
- [ ] UI de personalización para admin del tenant

### Fase 3
- [ ] Sistema de documentación con interpolación
- [ ] Screenshots por marca
- [ ] Override de documentación por tenant
- [ ] Locale adicional: Português

### Fase 4
- [ ] Editor WYSIWYG de guías para tenants
- [ ] Import/export de locales (CSV/JSON)
- [ ] Auto-detección de idioma por browser
- [ ] Pluralización avanzada (CLDR rules)

---

*Galappxy — Propuesta técnica i18n · Febrero 2026*
