Internationalization - PlugImt/transat-app GitHub Wiki

Internationalization (i18n)

🌍 Overview

Transat 2.0 supports multiple languages to accommodate the diverse international student body at IMT Atlantique. The app uses i18next with React Native for comprehensive internationalization support.

πŸ—£οΈ Supported Languages

The app currently supports 6 languages:

  • πŸ‡¬πŸ‡§ English (en) - Default language
  • πŸ‡«πŸ‡· French (fr) - Primary campus language
  • πŸ‡ͺπŸ‡Έ Spanish (es)
  • πŸ‡©πŸ‡ͺ German (de)
  • πŸ‡΅πŸ‡Ή Portuguese (pt)
  • πŸ‡¨πŸ‡³ Chinese (zh)

πŸ› οΈ Technical Implementation

i18next Configuration

The internationalization system is configured in src/i18n.ts:

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import * as Localization from 'expo-localization';

// Import translation resources
import en from '../locales/en/translation.json';
import fr from '../locales/fr/translation.json';
import es from '../locales/es/translation.json';
import de from '../locales/de/translation.json';
import pt from '../locales/pt/translation.json';
import zh from '../locales/zh/translation.json';

const resources = {
  en: { translation: en },
  fr: { translation: fr },
  es: { translation: es },
  de: { translation: de },
  pt: { translation: pt },
  zh: { translation: zh }
};

i18n
  .use(initReactI18next)
  .init({
    resources,
    lng: Localization.locale.split('-')[0], // Auto-detect system language
    fallbackLng: 'en',
    interpolation: {
      escapeValue: false
    },
    react: {
      useSuspense: false
    }
  });

export default i18n;

Expo Localization Integration

The app automatically detects the user's system language using expo-localization:

import * as Localization from 'expo-localization';

// Auto-detect system language
const systemLanguage = Localization.locale.split('-')[0];

// Fallback to English if system language not supported
const initialLanguage = resources[systemLanguage] ? systemLanguage : 'en';

πŸ“ Translation File Structure

Translation files are organized in the locales/ directory:

locales/
β”œβ”€β”€ en/
β”‚   β”œβ”€β”€ translation.json      # Main translations
β”‚   β”œβ”€β”€ common.json          # Common UI elements
β”‚   β”œβ”€β”€ auth.json            # Authentication screens
β”‚   β”œβ”€β”€ services.json        # Campus services
β”‚   └── errors.json          # Error messages
β”œβ”€β”€ fr/
β”‚   β”œβ”€β”€ translation.json
β”‚   β”œβ”€β”€ common.json
β”‚   β”œβ”€β”€ auth.json
β”‚   β”œβ”€β”€ services.json
β”‚   └── errors.json
└── [other languages...]

Translation JSON Structure

Example translation file structure:

{
  "common": {
    "welcome": "Welcome",
    "loading": "Loading...",
    "error": "An error occurred",
    "retry": "Retry",
    "save": "Save",
    "cancel": "Cancel"
  },
  "auth": {
    "login": "Sign In",
    "register": "Sign Up",
    "email": "Email",
    "password": "Password",
    "forgotPassword": "Forgot Password?",
    "loginError": "Invalid email or password"
  },
  "home": {
    "title": "Home",
    "weather": {
      "title": "Weather",
      "loading": "Loading weather..."
    },
    "restaurant": {
      "title": "Restaurant",
      "lunch": "Lunch",
      "dinner": "Dinner",
      "noMenu": "No menu available"
    },
    "washingMachine": {
      "title": "Washing Machines",
      "available": "Available",
      "occupied": "Occupied",
      "outOfOrder": "Out of Order"
    }
  },
  "services": {
    "title": "Services",
    "restaurant": "Restaurant",
    "washingMachines": "Washing Machines",
    "clubs": "Clubs",
    "games": "Games"
  }
}

πŸ”§ Usage in Components

useTranslation Hook

The useTranslation hook is used throughout the app for accessing translations:

import { useTranslation } from 'react-i18next';

export const ExampleComponent = () => {
  const { t } = useTranslation();

  return (
    <View>
      <Text>{t('common.welcome')}</Text>
      <Button title={t('common.save')} onPress={handleSave} />
    </View>
  );
};

Translation with Parameters

Support for dynamic content injection:

// Translation with parameters
const { t } = useTranslation();

// In translation file:
// "welcome_user": "Welcome, {{username}}!"

return (
  <Text>{t('welcome_user', { username: user.name })}</Text>
);

Pluralization Support

i18next supports pluralization for different languages:

{
  "itemCount": "{{count}} item",
  "itemCount_plural": "{{count}} items"
}
// Usage
<Text>{t('itemCount', { count: items.length })}</Text>

Namespace Organization

Organize translations by feature using namespaces:

// Load specific namespace
const { t } = useTranslation('auth');

// Access auth-specific translations
<Text>{t('login')}</Text> // auth:login

🌐 Crowdin Integration

The app uses Crowdin for collaborative translation management.

Translation Workflow

  1. Source Updates: Developers update English translations
  2. Crowdin Sync: Changes are pushed to Crowdin platform
  3. Community Translation: Volunteers translate to other languages
  4. Review Process: Native speakers review translations
  5. Download: Approved translations are pulled back to the app

Crowdin Configuration

# crowdin.yml
project_id: "transat"
api_token_env: "CROWDIN_API_TOKEN"
base_path: "."

files:
  - source: "/locales/en/**/*.json"
    translation: "/locales/%two_letters_code%/**/*.json"
    ignore:
      - "node_modules/**/*"

Contributing Translations

Community members can contribute translations through the Crowdin project:

Crowdin Project: https://crowdin.com/project/transat

πŸ“± Language Switching

Runtime Language Switching

Users can change the app language through settings:

import i18n from '@/src/i18n';

export const LanguageSelector = () => {
  const { t } = useTranslation();
  const currentLanguage = i18n.language;

  const changeLanguage = (languageCode: string) => {
    i18n.changeLanguage(languageCode);
    // Optionally persist the choice
    AsyncStorage.setItem('user_language', languageCode);
  };

  return (
    <Picker
      selectedValue={currentLanguage}
      onValueChange={changeLanguage}
    >
      <Picker.Item label="English" value="en" />
      <Picker.Item label="FranΓ§ais" value="fr" />
      <Picker.Item label="EspaΓ±ol" value="es" />
      <Picker.Item label="Deutsch" value="de" />
      <Picker.Item label="PortuguΓͺs" value="pt" />
      <Picker.Item label="δΈ­ζ–‡" value="zh" />
    </Picker>
  );
};

Persistent Language Settings

Store user language preference locally:

import AsyncStorage from '@react-native-async-storage/async-storage';

// Save language preference
const saveLanguagePreference = async (language: string) => {
  await AsyncStorage.setItem('user_language', language);
};

// Load language preference on app start
const loadLanguagePreference = async () => {
  const savedLanguage = await AsyncStorage.getItem('user_language');
  if (savedLanguage && resources[savedLanguage]) {
    i18n.changeLanguage(savedLanguage);
  }
};

🎯 Best Practices

Translation Keys

  1. Hierarchical Structure: Use nested keys for organization

    {
      "screens": {
        "home": {
          "title": "Home",
          "subtitle": "Welcome back"
        }
      }
    }
  2. Descriptive Naming: Use clear, descriptive key names

    {
      "button_save_profile": "Save Profile",
      "error_network_connection": "Network connection failed"
    }
  3. Context Indication: Include context in key names

    {
      "modal_title_delete_account": "Delete Account",
      "page_title_user_settings": "User Settings"
    }

Component Guidelines

  1. Extract All Text: Never hardcode text in components

    // ❌ Bad
    <Text>Welcome to Transat</Text>
    
    // βœ… Good
    <Text>{t('welcome_message')}</Text>
  2. Use Meaningful Defaults: Provide fallback text

    <Text>{t('welcome_message', 'Welcome')}</Text>
  3. Handle Missing Translations: Graceful degradation

    const title = t('page_title') || 'Default Title';

Cultural Considerations

  1. Date/Time Formatting: Use locale-specific formatting

    import { format } from 'date-fns';
    import { fr, en, es } from 'date-fns/locale';
    
    const formatDate = (date: Date, language: string) => {
      const locales = { fr, en, es };
      return format(date, 'PPP', { locale: locales[language] });
    };
  2. Number Formatting: Respect locale conventions

    const formatNumber = (number: number, language: string) => {
      return new Intl.NumberFormat(language).format(number);
    };
  3. Text Direction: Support RTL languages (future consideration)

πŸ”„ Translation Updates

Adding New Translations

  1. Add to English Source:

    {
      "new_feature": {
        "title": "New Feature",
        "description": "This is a new feature"
      }
    }
  2. Update Crowdin: Push changes to translation platform

  3. Use in Components:

    <Text>{t('new_feature.title')}</Text>

Translation Validation

Automated checks for translation completeness:

// Check for missing translations
const validateTranslations = (baseLanguage: object, targetLanguage: object) => {
  const missingKeys = [];
  // Compare translation objects
  // Report missing keys
  return missingKeys;
};

πŸ“Š Translation Statistics

Current Translation Coverage

  • πŸ‡¬πŸ‡§ English: 100% (Source language)
  • πŸ‡«πŸ‡· French: ~95% (Primary target)
  • πŸ‡ͺπŸ‡Έ Spanish: ~85%
  • πŸ‡©πŸ‡ͺ German: ~80%
  • πŸ‡΅πŸ‡Ή Portuguese: ~75%
  • πŸ‡¨πŸ‡³ Chinese: ~70%

Quality Metrics

  • Consistency: Term glossary maintained
  • Context: Screenshots provided for translators
  • Reviews: Native speaker validation
  • Updates: Regular synchronization with development

Next Steps: Learn about the backend integration and API services in Services & APIs documentation.

⚠️ **GitHub.com Fallback** ⚠️