# Lokalize

[![Commercial](https://img.shields.io/badge/License-Commercial-red.svg)](LICENSE.md)
[![Cockpit CMS](https://img.shields.io/badge/Cockpit%20CMS-Addon-blue.svg)](https://getcockpit.com)
[![Translation](https://img.shields.io/badge/Feature-i18n%20%7C%20Translation-green.svg)](#features)

> **Professional internationalization and translation management for Cockpit CMS**

Lokalize is a commercial addon that transforms Cockpit CMS into a powerful translation management system. Handle multi-language projects, automate translations with AI services, manage translation workflows, and deliver localized content with enterprise-grade internationalization features.

## ✨ Features

### 🌍 **Multi-Language Project Management**
- **Translation Projects**: Create and manage unlimited translation projects
- **Multi-Locale Support**: Handle dozens of languages within single projects
- **Hierarchical Keys**: Organize translations with dot notation namespacing
- **Status Tracking**: Monitor translation completion per locale
- **Team Collaboration**: Coordinate translator workflows and assignments

### 🤖 **AI-Powered Translation Services**
- **DeepL Integration**: Premium neural translation with 30+ languages
- **LibreTranslate**: Open-source, self-hosted translation service
- **Automatic Translation**: One-click translation for rapid localization
- **HTML Content Support**: Intelligent handling of markup and formatting
- **Source Detection**: Automatic source language identification

### 📊 **Translation Workflow Management**
- **Key Organization**: Hierarchical key structure with dot notation
- **Plural Forms**: Handle complex plural rules for different languages
- **CSV Import/Export**: Bulk translation management via admin interface
- **Progress Tracking**: Visual completion status per locale
- **Version Control**: Track translation changes and history

### 🔧 **Developer APIs**
- **REST API**: Fetch translations for frontend applications
- **GraphQL**: Advanced querying with nested key support
- **Locale Filtering**: Get translations for specific languages
- **Nested Structure**: Support for hierarchical translation keys
- **Real-time Updates**: Dynamic translation loading and caching

### 📈 **Enterprise Features**
- **Bulk Operations**: Mass import/export via admin interface
- **Team Permissions**: Role-based access to translation projects
- **Integration Ready**: API-first design for headless architectures
- **Performance Optimized**: Caching and efficient data structures
- **Scalable Architecture**: Handle large translation datasets

## 🚀 Quick Start

### 1. Installation

Lokalize is a commercial addon included with Cockpit CMS Pro licenses.

### 2. Configure Translation Service (Optional)

For automatic translations, configure a translation service in `config/config.php`:

**DeepL Configuration:**
```php
<?php
return [
    // ... other config options
    
    'lokalize' => [
        'translation' => [
            'service' => 'deepl',
            'apiKey' => 'your-deepl-api-key'
        ]
    ]
];
```

**LibreTranslate Configuration:**
```php
<?php
return [
    'lokalize' => [
        'translation' => [
            'service' => 'libretranslate',
            'url' => 'https://libretranslate.com',  // or your self-hosted instance
            'apiKey' => 'your-api-key'             // if required by your instance
        ]
    ]
];
```

### 3. Create Your First Translation Project

1. Navigate to **Lokalize** in the admin sidebar
2. Click **"Create Project"**
3. Configure your project:

```php
Project Configuration:
- Name: "website"
- Locales: 
  * English (en) - default
  * Spanish (es)
  * French (fr)
- Keys:
  * welcome.title
  * nav.home
  * nav.about
  * button.save
```

### 4. Add Translation Keys

```php
// Example translation keys structure
Keys:
- welcome.title
- welcome.subtitle
- nav.home
- nav.about
- nav.contact
- button.save
- button.cancel
- message.success
- error.validation
- items.count (with plural support)
```

### 5. Use Translations in Your Application

**REST API:**
```javascript
// Fetch all translations for English
fetch('/api/lokalize/project/website?locale=en')
    .then(response => response.json())
    .then(translations => {
        console.log(translations['welcome.title']); // "Welcome"
        console.log(translations['nav.home']);      // "Home"
    });
```

**GraphQL:**
```graphql
query GetTranslations {
    lokalize(project: "website", locale: "en", nested: 1) {
        welcome {
            title
            subtitle
        }
        nav {
            home
            about
            contact
        }
        button {
            save
            cancel
        }
    }
}
```

## 📋 Project Configuration

### Basic Project Setup

```php
// Project structure
$project = [
    'name' => 'website',                    // Required: Project identifier
    'label' => 'Website Translations',     // Optional: Display name
    'description' => 'Main website i18n',  // Optional: Project description
    
    'locales' => [
        ['i18n' => 'en', 'name' => 'English'],     // Default locale
        ['i18n' => 'es', 'name' => 'Spanish'],
        ['i18n' => 'fr', 'name' => 'French'],
        ['i18n' => 'de', 'name' => 'German'],
        ['i18n' => 'ja', 'name' => 'Japanese']
    ],
    
    'keys' => [
        ['name' => 'welcome.title', 'plural' => false],
        ['name' => 'items.count', 'plural' => true],
        ['name' => 'nav.home', 'plural' => false]
    ]
];
```

### Translation Values Structure

```php
'values' => [
    'en' => [
        'welcome.title' => ['value' => 'Welcome to our site'],
        'items.count' => ['value' => 'item', 'plural' => 'items'],
        'nav.home' => ['value' => 'Home'],
        'nav.about' => ['value' => 'About Us']
    ],
    'es' => [
        'welcome.title' => ['value' => 'Bienvenido a nuestro sitio'],
        'items.count' => ['value' => 'artículo', 'plural' => 'artículos'],
        'nav.home' => ['value' => 'Inicio'],
        'nav.about' => ['value' => 'Acerca de']
    ],
    'fr' => [
        'welcome.title' => ['value' => 'Bienvenue sur notre site'],
        'items.count' => ['value' => 'article', 'plural' => 'articles'],
        'nav.home' => ['value' => 'Accueil'],
        'nav.about' => ['value' => 'À propos']
    ]
]
```

### Completion Status Tracking

```php
'status' => [
    'en' => 100,    // 100% complete (default locale)
    'es' => 85,     // 85% complete
    'fr' => 70,     // 70% complete
    'de' => 45,     // 45% complete
    'ja' => 0       // 0% complete (not started)
]
```

## 🔧 Translation Services

### DeepL Integration

**Setup:**
1. Sign up for [DeepL API](https://www.deepl.com/pro-api)
2. Get your API key
3. Configure in Cockpit:

```php
'lokalize' => [
    'translation' => [
        'service' => 'deepl',
        'apiKey' => 'your-deepl-api-key'  // Free or Pro tier
    ]
]
```

**Supported Languages:**
- **Source**: Auto-detection or specify (EN, DE, FR, ES, PT, IT, NL, PL, RU, JA, ZH)
- **Target**: 30+ languages including EN-US, EN-GB, DE, FR, ES, PT-PT, PT-BR, IT, NL, PL, RU, JA, ZH

**Features:**
- ✅ High-quality neural translations
- ✅ Document translation
- ✅ Formal/informal tone control
- ✅ Translation alternatives
- ✅ Usage statistics and limits

### LibreTranslate Integration

**Self-Hosted Setup:**
```bash
# Install LibreTranslate
pip install libretranslate

# Run locally
libretranslate --host 0.0.0.0 --port 5000
```

**Configuration:**
```php
'lokalize' => [
    'translation' => [
        'service' => 'libretranslate',
        'url' => 'http://localhost:5000',        // Your LibreTranslate instance
        'apiKey' => 'your-api-key'               // If API key is required
    ]
]
```

**Benefits:**
- ✅ Open source and self-hosted
- ✅ No external API dependencies
- ✅ Custom translation models
- ✅ Privacy and data control
- ✅ HTML content support
- ✅ Unlimited usage

## 🎯 Frontend Integration

### JavaScript/TypeScript

```javascript
class TranslationManager {
    constructor(project, locale = 'en') {
        this.project = project;
        this.locale = locale;
        this.translations = {};
        this.cache = new Map();
    }
    
    async loadTranslations(locale = this.locale) {
        const cacheKey = `${this.project}-${locale}`;
        
        if (this.cache.has(cacheKey)) {
            return this.cache.get(cacheKey);
        }
        
        try {
            const response = await fetch(`/api/lokalize/project/${this.project}?locale=${locale}&nested=1`);
            const translations = await response.json();
            
            this.cache.set(cacheKey, translations);
            this.translations = translations;
            
            return translations;
        } catch (error) {
            console.error('Failed to load translations:', error);
            return {};
        }
    }
    
    t(key, params = {}) {
        const value = this.getValue(key);
        
        if (!value) {
            console.warn(`Translation key not found: ${key}`);
            return key;
        }
        
        return this.interpolate(value, params);
    }
    
    getValue(key) {
        const keys = key.split('.');
        let value = this.translations;
        
        for (const k of keys) {
            if (value && typeof value === 'object' && k in value) {
                value = value[k];
            } else {
                return null;
            }
        }
        
        return value;
    }
    
    interpolate(text, params) {
        return text.replace(/\{\{(\w+)\}\}/g, (match, key) => {
            return params[key] !== undefined ? params[key] : match;
        });
    }
    
    plural(key, count, params = {}) {
        const singular = this.getValue(key);
        const plural = this.getValue(`${key}_plural`);
        
        const value = count === 1 ? singular : (plural || singular);
        return this.interpolate(value, { ...params, count });
    }
}

// Usage
const i18n = new TranslationManager('website', 'en');

await i18n.loadTranslations();

console.log(i18n.t('welcome.title'));                           // "Welcome to our site"
console.log(i18n.t('message.hello', { name: 'John' }));        // "Hello, John!"
console.log(i18n.plural('items.count', 1));                    // "1 item"
console.log(i18n.plural('items.count', 5));                    // "5 items"
```

### Vue.js Plugin

```javascript
// vue-lokalize-plugin.js
export default {
    install(app, options) {
        const { project, locale = 'en' } = options;
        
        app.config.globalProperties.$t = function(key, params = {}) {
            return this.$lokalize.t(key, params);
        };
        
        app.config.globalProperties.$plural = function(key, count, params = {}) {
            return this.$lokalize.plural(key, count, params);
        };
        
        app.provide('lokalize', new TranslationManager(project, locale));
    }
};

// main.js
import { createApp } from 'vue';
import LokalizePlugin from './vue-lokalize-plugin.js';

const app = createApp(App);

app.use(LokalizePlugin, {
    project: 'website',
    locale: 'en'
});

app.mount('#app');
```

```vue
<!-- Component usage -->
<template>
    <div>
        <h1>{{ $t('welcome.title') }}</h1>
        <p>{{ $t('message.greeting', { name: user.name }) }}</p>
        <span>{{ $plural('items.count', itemCount) }}</span>
        
        <nav>
            <router-link to="/">{{ $t('nav.home') }}</router-link>
            <router-link to="/about">{{ $t('nav.about') }}</router-link>
            <router-link to="/contact">{{ $t('nav.contact') }}</router-link>
        </nav>
    </div>
</template>

<script>
export default {
    data() {
        return {
            user: { name: 'John' },
            itemCount: 5
        };
    }
};
</script>
```

### React Hook

```jsx
import { useState, useEffect, useContext, createContext } from 'react';

const LokalizeContext = createContext();

export const LokalizeProvider = ({ children, project, locale = 'en' }) => {
    const [translations, setTranslations] = useState({});
    const [currentLocale, setCurrentLocale] = useState(locale);
    const [loading, setLoading] = useState(true);
    
    const loadTranslations = async (locale) => {
        setLoading(true);
        try {
            const response = await fetch(`/api/lokalize/project/${project}?locale=${locale}&nested=1`);
            const data = await response.json();
            setTranslations(data);
        } catch (error) {
            console.error('Failed to load translations:', error);
        } finally {
            setLoading(false);
        }
    };
    
    useEffect(() => {
        loadTranslations(currentLocale);
    }, [currentLocale, project]);
    
    const t = (key, params = {}) => {
        const keys = key.split('.');
        let value = translations;
        
        for (const k of keys) {
            if (value && typeof value === 'object' && k in value) {
                value = value[k];
            } else {
                console.warn(`Translation key not found: ${key}`);
                return key;
            }
        }
        
        return interpolate(value, params);
    };
    
    const plural = (key, count, params = {}) => {
        const singular = t(key);
        const plural = t(`${key}_plural`);
        
        const value = count === 1 ? singular : plural;
        return interpolate(value, { ...params, count });
    };
    
    const interpolate = (text, params) => {
        return text.replace(/\{\{(\w+)\}\}/g, (match, key) => {
            return params[key] !== undefined ? params[key] : match;
        });
    };
    
    const setLocale = (locale) => {
        setCurrentLocale(locale);
    };
    
    return (
        <LokalizeContext.Provider value={{
            t,
            plural,
            setLocale,
            currentLocale,
            loading,
            translations
        }}>
            {children}
        </LokalizeContext.Provider>
    );
};

export const useLokalize = () => {
    const context = useContext(LokalizeContext);
    if (!context) {
        throw new Error('useLokalize must be used within a LokalizeProvider');
    }
    return context;
};

// Usage
function App() {
    return (
        <LokalizeProvider project="website" locale="en">
            <MainComponent />
        </LokalizeProvider>
    );
}

function MainComponent() {
    const { t, plural, setLocale, currentLocale } = useLokalize();
    const [itemCount, setItemCount] = useState(5);
    
    return (
        <div>
            <h1>{t('welcome.title')}</h1>
            <p>{t('message.greeting', { name: 'John' })}</p>
            <span>{plural('items.count', itemCount)}</span>
            
            <select value={currentLocale} onChange={(e) => setLocale(e.target.value)}>
                <option value="en">English</option>
                <option value="es">Español</option>
                <option value="fr">Français</option>
            </select>
        </div>
    );
}
```



## 🔧 API Reference

### REST API

**Get Project Translations:**
```http
GET /api/lokalize/project/{name}

Query Parameters:
- locale: Filter by specific locale (optional)
- nested: Return nested structure (0 or 1)

Examples:
GET /api/lokalize/project/website
GET /api/lokalize/project/website?locale=es
GET /api/lokalize/project/website?nested=1
```

**Response Format:**
```json
{
    "en": {
        "welcome.title": "Welcome",
        "nav.home": "Home",
        "items.count": "item",
        "items.count_plural": "items"
    },
    "es": {
        "welcome.title": "Bienvenido",
        "nav.home": "Inicio",
        "items.count": "artículo",
        "items.count_plural": "artículos"
    }
}
```

**Nested Structure (nested=1):**
```json
{
    "en": {
        "welcome": {
            "title": "Welcome"
        },
        "nav": {
            "home": "Home"
        },
        "items": {
            "count": "item",
            "count_plural": "items"
        }
    }
}
```

### GraphQL API

**Query Structure:**
```graphql
query GetTranslations($project: String!, $locale: String, $nested: Int) {
    lokalize(project: $project, locale: $locale, nested: $nested)
}
```

**Variables:**
```json
{
    "project": "website",
    "locale": "en",
    "nested": 1
}
```

**Example Queries:**
```graphql
# Get all translations for a project
query {
    lokalize(project: "website") {
        # Returns flat structure with all locales
    }
}

# Get specific locale with nested structure
query {
    lokalize(project: "website", locale: "en", nested: 1) {
        # Returns nested structure for English only
    }
}
```

## 🐛 Troubleshooting

### Common Issues

**❌ "Translation service not configured"**
- Ensure translation service is configured in `config/config.php`
- Verify API keys are correct and have sufficient credits
- Check network connectivity to translation services

**❌ "Project not found" API Error**
- Verify project name matches exactly (case-sensitive)
- Check that the project exists in the Lokalize admin
- Ensure proper API permissions are configured

**❌ "DeepL API quota exceeded"**
- Check your DeepL account usage and limits
- Upgrade to Pro tier for higher limits
- Consider using LibreTranslate as an alternative

**❌ "LibreTranslate connection failed"**
- Verify LibreTranslate service is running
- Check URL configuration (include protocol: http/https)
- Ensure firewall allows connections to LibreTranslate port

### Debug Mode

Enable debug logging to troubleshoot translation issues:

```php
// config/config.php
'debug' => true,

// Check API responses
$translation = $app->module('lokalize')->translate('Hello world', 'es');
var_dump($translation);
```

### Testing Translations

Test your translation API endpoints:

```bash
# Test project endpoint
curl "https://yoursite.com/api/lokalize/project/website?locale=en"

# Test with nested structure
curl "https://yoursite.com/api/lokalize/project/website?nested=1"

# Test GraphQL
curl -X POST "https://yoursite.com/api/gql" \
  -H "Content-Type: application/json" \
  -d '{"query": "{ lokalize(project: \"website\", locale: \"en\") }"}'
```

### Performance Optimization

1. **Enable Caching**: Cache translations on the frontend
2. **Use Nested Structure**: Reduce API calls with nested=1
3. **Locale-Specific Requests**: Only fetch needed locales
4. **Batch Operations**: Use CSV import for bulk updates
5. **Monitor API Usage**: Track DeepL/LibreTranslate usage

## 📄 License

This is a commercial addon for Cockpit CMS. See [LICENSE.md](LICENSE.md) for full terms.

## 🙏 Credits

Lokalize is developed by [Agentejo](https://agentejo.com) as part of the Cockpit CMS Pro ecosystem.

---

**Ready to go global?** Set up Lokalize and start managing your multi-language content with professional translation workflows today!