# All files / services TranslationService.js

0% Statements 0/54
0% Branches 0/37
0% Functions 0/11
0% Lines 0/53

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143

     /**
     * Translation Service
     * Centralized translation management with i18n support
     */

    const fs = require('fs');
    const path = require('path');
    const { pool } = require('../config/database');

    class TranslationService {
      constructor() {
        this.translations = {};
        this.defaultLanguage = 'en';
        this.loadTranslations();
        this.refreshDefaultLanguage();
      }

      /**
       * Load all translation files
       */
      loadTranslations() {
        const localesPath = path.join(__dirname, '../locales');

        try {
          const files = fs.readdirSync(localesPath);

          files.forEach(file => {
            if (file.endsWith('.json')) {
              const lang = file.replace('.json', '');
              const filePath = path.join(localesPath, file);
              this.translations[lang] = JSON.parse(fs.readFileSync(filePath, 'utf8'));
            }
          });
        } catch (error) {
          console.error('Error loading translations:', error.message);
        }
      }

      async refreshDefaultLanguage() {
        try {
          // Check if table exists first
          const [tables] = await pool.execute(
            `SHOW TABLES LIKE 'system_settings_kv'`
          );

          if (tables.length === 0) {
            // Table doesn't exist yet, use default
            return;
          }

          const [rows] = await pool.execute(
            `SELECT setting_value FROM system_settings_kv WHERE setting_key = 'default_language' LIMIT 1`
          );
          if (rows && rows[0] && rows[0].setting_value) {
            this.defaultLanguage = rows[0].setting_value;
          }
        } catch (error) {
          // Silently fail during initialization, table may not exist yet
          // This is expected during first startup
        }
      }

      setDefaultLanguage(language) {
        if (language && typeof language === 'string') {
          this.defaultLanguage = language;
        }
      }

      /**
       * Get translation for a key
       * @param {string} key - Translation key (e.g., 'errors.not_found')
       * @param {string} lang - Language code (default: 'en')
       * @param {object} params - Parameters for interpolation
       * @returns {string} Translated text
       */
      t(key, lang = 'en', params = {}) {
        const keys = key.split('.');
        let translation = this.translations[lang] || this.translations[this.defaultLanguage];

        for (const k of keys) {
          if (translation && translation[k]) {
            translation = translation[k];
          } else {
            // Fallback to default language
            translation = this.translations[this.defaultLanguage];
            for (const k2 of keys) {
              if (translation && translation[k2]) {
                translation = translation[k2];
              } else {
                return key; // Return key if translation not found
              }
            }
            break;
          }
        }

        // Interpolate parameters
        if (typeof translation === 'string' && params) {
          Object.keys(params).forEach(param => {
            translation = translation.replace(new RegExp(`{{${param}}}`, 'g'), params[param]);
          });
        }

        return translation;
      }

      /**
       * Get user's preferred language from request
       * @param {object} req - Express request object
       * @returns {string} Language code
       */
      getLanguage(req) {
        // Priority: 1. User preference, 2. Accept-Language header, 3. Default
        if (req.user && req.user.preferred_language) {
          return req.user.preferred_language;
        }

        if (req.headers['accept-language']) {
          const lang = req.headers['accept-language'].split(',')[0].split('-')[0];
          if (this.translations[lang]) {
            return lang;
          }
        }

        return this.defaultLanguage;
      }

      /**
       * Middleware to add translation function to request
       */
      middleware() {
        return (req, res, next) => {
          const lang = this.getLanguage(req);
          req.t = (key, params) => this.t(key, lang, params);
          req.lang = lang;
          next();
        };
      }
    }

    // Export singleton instance
    module.exports = new TranslationService();