Internationalization - PlugImt/transat-app GitHub Wiki
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.
The app currently supports 6 languages:
- π¬π§ English (
en
) - Default language - π«π· French (
fr
) - Primary campus language - πͺπΈ Spanish (
es
) - π©πͺ German (
de
) - π΅πΉ Portuguese (
pt
) - π¨π³ Chinese (
zh
)
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;
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 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...]
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"
}
}
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>
);
};
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>
);
i18next supports pluralization for different languages:
{
"itemCount": "{{count}} item",
"itemCount_plural": "{{count}} items"
}
// Usage
<Text>{t('itemCount', { count: items.length })}</Text>
Organize translations by feature using namespaces:
// Load specific namespace
const { t } = useTranslation('auth');
// Access auth-specific translations
<Text>{t('login')}</Text> // auth:login
The app uses Crowdin for collaborative translation management.
- Source Updates: Developers update English translations
- Crowdin Sync: Changes are pushed to Crowdin platform
- Community Translation: Volunteers translate to other languages
- Review Process: Native speakers review translations
- Download: Approved translations are pulled back to the app
# 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/**/*"
Community members can contribute translations through the Crowdin project:
Crowdin Project: https://crowdin.com/project/transat
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>
);
};
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);
}
};
-
Hierarchical Structure: Use nested keys for organization
{ "screens": { "home": { "title": "Home", "subtitle": "Welcome back" } } }
-
Descriptive Naming: Use clear, descriptive key names
{ "button_save_profile": "Save Profile", "error_network_connection": "Network connection failed" }
-
Context Indication: Include context in key names
{ "modal_title_delete_account": "Delete Account", "page_title_user_settings": "User Settings" }
-
Extract All Text: Never hardcode text in components
// β Bad <Text>Welcome to Transat</Text> // β Good <Text>{t('welcome_message')}</Text>
-
Use Meaningful Defaults: Provide fallback text
<Text>{t('welcome_message', 'Welcome')}</Text>
-
Handle Missing Translations: Graceful degradation
const title = t('page_title') || 'Default Title';
-
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] }); };
-
Number Formatting: Respect locale conventions
const formatNumber = (number: number, language: string) => { return new Intl.NumberFormat(language).format(number); };
-
Text Direction: Support RTL languages (future consideration)
-
Add to English Source:
{ "new_feature": { "title": "New Feature", "description": "This is a new feature" } }
-
Update Crowdin: Push changes to translation platform
-
Use in Components:
<Text>{t('new_feature.title')}</Text>
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;
};
- π¬π§ English: 100% (Source language)
- π«π· French: ~95% (Primary target)
- πͺπΈ Spanish: ~85%
- π©πͺ German: ~80%
- π΅πΉ Portuguese: ~75%
- π¨π³ Chinese: ~70%
- 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.