This document describes the comprehensive accessibility and internationalization (i18n) system implemented in the StrellerMinds Backend. The system ensures WCAG 2.1 AA compliance and supports 15+ languages including RTL language support.
- Internationalization (i18n)
- Accessibility Features
- RTL Language Support
- Testing & Compliance
- API Endpoints
- Implementation Examples
The system supports the following languages:
- English (en) - English, US
- Spanish (es) - Español, ES
- French (fr) - Français, FR
- German (de) - Deutsch, DE
- Italian (it) - Italiano, IT
- Portuguese (pt) - Português, PT
- Russian (ru) - Русский, RU
- Japanese (ja) - 日本語, JP
- Chinese (zh) - 中文, CN
- Korean (ko) - 한국어, KR
- Arabic (ar) - العربية, SA - RTL
- Hindi (hi) - हिन्दी, IN
- Thai (th) - ไทย, TH
- Vietnamese (vi) - Tiếng Việt, VN
- Turkish (tr) - Türkçe, TR
The I18nService manages all translation functionality:
// Inject the service
constructor(private readonly i18nService: I18nService) {}
// Translate a single key
const text = this.i18nService.translate('common.welcome', 'en');
// Translate with parameters
const text = this.i18nService.translate('errors.passwordTooShort', 'es', {});
// Get multiple translations
const translations = this.i18nService.translateMultiple(
['common.welcome', 'common.goodbye'],
'fr'
);
// Detect language from header
const language = this.i18nService.detectLanguageFromHeader('en-US,en;q=0.9,fr;q=0.8');
// Check if language is RTL
const isRTL = this.i18nService.isRTL('ar'); // true
// Get language metadata
const metadata = this.i18nService.getLanguageMetadata('ja');The LanguageDetectionMiddleware automatically detects and sets the user's language based on:
- URL query parameter:
?lang=en - Language cookie: Previously set language preference
- Accept-Language header: Browser/client preference
- Default: Falls back to English
The detected language is available on the request object:
@Get('profile')
getProfile(@Req() req) {
const language = req['language']; // Detected language
const isRTL = req['isRTL']; // RTL flag
const locale = req['locale']; // Full locale string (e.g., 'ar-SA')
}Each language has a JSON translation file at src/i18n/translations/{lang}.json:
{
"common": {
"welcome": "Welcome",
"goodbye": "Goodbye"
},
"auth": {
"login": "Login",
"password": "Password"
},
"errors": {
"required": "This field is required"
}
}- GET
/i18n/languages- Get all supported languages - GET
/i18n/translations?lang=en&keys=common.welcome,auth.login- Get translations - GET
/i18n/detect- Detect language from Accept-Language header - GET
/i18n/translate?key=common.welcome&lang=es- Translate a single key
The system implements comprehensive WCAG 2.1 AA compliance including:
- Non-text content has text alternatives (alt text)
- Color contrast ratio of at least 4.5:1 for normal text
- Flexible text sizing
- All functionality available from keyboard
- No keyboard traps
- Logical focus order
- Skip links for keyboard navigation
- Language of page is identified
- Clear error identification
- Error prevention for critical transactions
- Name, role, and value of components are available
- Status messages conveyed to assistive technologies
The AccessibilityService provides utilities for building accessible components:
// Build ARIA attributes
const ariaAttrs = this.accessibilityService.buildAriaAttributes({
role: AriaRole.BUTTON,
label: 'Close dialog',
ariaPressed: false,
ariaExpanded: false,
});
// Create keyboard navigation handler
const handler = this.accessibilityService.createKeyboardNavigationHandler({
onEscape: () => this.closeDialog(),
onEnter: () => this.submitForm(),
onArrowUp: () => this.selectPrevious(),
onArrowDown: () => this.selectNext(),
});
// Check color contrast
const contrast = this.accessibilityService.checkContrastRatio('#000000', '#FFFFFF');
console.log(contrast.meetsAA); // true
// Get skip navigation links
const skipLinks = this.accessibilityService.getSkipNavigationLinks();
// Get WCAG checklist
const checklist = this.accessibilityService.getWCAGComplianceChecklist();Use decorators to mark accessible components:
import {
AccessibleName,
ScreenReaderOptimized,
KeyboardNavigable,
WCAGCompliance,
} from './accessibility/decorators/accessibility.decorators';
@Controller('users')
@ScreenReaderOptimized()
@WCAGCompliance('AA')
export class UserController {
@Get('profile')
@KeyboardNavigable('#user-profile')
@AccessibleName('User Profile')
getProfile() {
// Implementation
}
}The system is fully optimized for screen readers:
- ARIA Landmarks: All major sections use semantic HTML or ARIA landmarks
- Live Regions: Dynamic content uses
aria-liveregions - Form Labels: All inputs have associated labels
- Error Messages: Errors are properly announced
- Skip Links: Users can skip navigation
Example ARIA implementation:
buildAriaAttributes({
role: AriaRole.ALERT,
ariaLive: AriaPoliteness.ASSERTIVE,
ariaAtomic: true,
label: 'Error message: Password too short',
})Full keyboard navigation support with proper focus management:
const keyboardHandler = this.accessibilityService.createKeyboardNavigationHandler({
onEscape: () => this.closeModal(),
onEnter: () => this.submitForm(),
onTab: () => this.moveFocus(),
onArrowUp: () => this.selectPreviousItem(),
onArrowDown: () => this.selectNextItem(),
onHome: () => this.selectFirstItem(),
onEnd: () => this.selectLastItem(),
});
document.addEventListener('keydown', keyboardHandler);The RTLService handles all right-to-left language requirements:
// Check if language is RTL
if (this.rtlService.isRTL('ar')) {
// Apply RTL styles
}
// Get text direction
const direction = this.rtlService.getDirection('ar'); // 'rtl'
// Get HTML attributes
const attrs = this.rtlService.getHtmlAttributes('ar');
// { lang: 'ar', dir: 'rtl' }
// Flip spacing based on direction
const spacing = this.rtlService.flipSpacing('ar', {
left: 16,
right: 8,
top: 4,
bottom: 4,
});
// { left: 8, right: 16, top: 4, bottom: 4 }
// Format numbers in language locale
const formatted = this.rtlService.formatNumber('ar', 1234.56); // '١٬٢٣٤٫٥٦'
// Format dates
const date = this.rtlService.formatDate('ar', new Date());
// Format currency
const price = this.rtlService.formatCurrency('ar', 99.99, 'SAR');
// Format lists
const list = this.rtlService.formatList('ar', ['item1', 'item2', 'item3']);- Arabic (ar)
- Hebrew (he)
- Persian/Farsi (fa)
- Urdu (ur)
-
Use logical CSS properties when possible:
margin-inline-startinstead ofmargin-leftpadding-inline-endinstead ofpadding-righttext-align: startinstead oftext-align: left
-
Use the RTL service for positioning:
const position = this.rtlService.getPosition('ar', { left: '16px', right: undefined, });
-
Set HTML dir attribute:
const attrs = this.rtlService.getHtmlAttributes('ar'); // Returns: { lang: 'ar', dir: 'rtl', ... }
The AccessibilityTestingService provides comprehensive testing utilities:
// Run comprehensive audit
const audit = this.testingService.runComprehensiveAudit(html);
// Validate WCAG compliance
const results = this.testingService.validateWCAGCompliance(html);
// Validate keyboard navigation
const keyboardResults = this.testingService.validateKeyboardNavigation(html);
// Validate screen reader compatibility
const screenReaderResults = this.testingService.validateScreenReaderCompat(html);
// Check WCAG 2.1 AA compliance
const isCompliant = this.testingService.meetsWCAG21AA(results);
// Generate report
const report = this.testingService.generateReport(results);The AccessibilityTestUtils provides detailed testing:
// Test keyboard navigation
const keyboardTest = this.testUtils.testKeyboardNavigation(html);
// Test screen reader compatibility
const srTest = this.testUtils.testScreenReaderCompat(html);
// Test color contrast
const contrast = this.testUtils.testColorContrast('#000000', '#FFFFFF');
// Validate form accessibility
const formTest = this.testUtils.validateFormAccessibility(html);
// Test image accessibility
const imageTest = this.testUtils.testImageAccessibility(html);
// Test heading structure
const headingTest = this.testUtils.testHeadingStructure(html);
// Generate comprehensive report
const report = this.testUtils.generateReport(html, css);- POST
/accessibility/audit- Run accessibility audit on HTML - GET
/accessibility/wcag-checklist- Get WCAG 2.1 AA checklist - POST
/accessibility/screen-reader-text- Generate screen reader text - GET
/accessibility/contrast?foreground=#000&background=#FFF- Check contrast ratio
GET /i18n/languages
GET /i18n/translations?lang=en&keys=common.welcome
GET /i18n/detect
GET /i18n/translate?key=common.welcome&lang=es
GET /accessibility/wcag-checklist
POST /accessibility/build-aria
POST /accessibility/screen-reader-text
GET /accessibility/contrast?foreground=#000&background=#FFF
GET /accessibility/skip-links
POST /accessibility/audit
GET /accessibility/overview
@Post('register')
async register(@Body() dto: RegisterDto, @Req() req) {
const language = req['language'];
const isRTL = req['isRTL'];
// Validate with localized error messages
if (!dto.email) {
const errorMsg = this.i18nService.translate(
'errors.required',
language,
);
throw new BadRequestException(errorMsg);
}
// Build ARIA attributes for form
const ariaAttrs = this.accessibilityService.buildAriaAttributes({
role: AriaRole.FORM,
labelledBy: 'form-title',
});
return {
success: true,
direction: this.rtlService.getDirection(language),
ariaAttributes: ariaAttrs,
};
}buildAccessibleButton(label: string, language: string) {
const ariaAttrs = this.accessibilityService.buildAriaAttributes({
role: AriaRole.BUTTON,
label: this.i18nService.translate(label, language),
ariaDisabled: false,
});
const textAlign = this.rtlService.getTextAlign(language);
return {
ariaAttributes: ariaAttrs,
textAlign,
direction: this.rtlService.getDirection(language),
};
}@Post('audit-content')
async auditContent(@Body() body: { html: string }) {
const report = this.testingService.generateReport(
this.testingService.runComprehensiveAudit(body.html),
);
return {
compliant: report.wcagCompliance.meets,
issues: report.summary,
recommendations: report.recommendations,
};
}@Post('set-language')
setLanguage(@Body() body: { language: string }, @Res() res: Response) {
const normalizedLang = this.i18nService.normalizeLanguageCode(body.language);
// Set language cookie
res.cookie('language', normalizedLang, {
httpOnly: true,
maxAge: 365 * 24 * 60 * 60 * 1000, // 1 year
});
// Get metadata
const metadata = this.i18nService.getLanguageMetadata(normalizedLang);
const isRTL = this.rtlService.isRTL(normalizedLang);
return {
language: normalizedLang,
metadata,
isRTL,
};
}-
Always include lang and dir attributes:
<html lang="{{ language }}" dir="{{ direction }}">
-
Use semantic HTML elements:
<nav role="navigation"> <main id="main-content"> <button aria-label="{{ aria-label }}">
-
Provide skip links:
<a href="#main-content" class="skip-link">Skip to main content</a>
-
Test with screen readers: NVDA (Windows), JAWS, VoiceOver (macOS)
-
Include language in all responses:
return { data: {...}, language: req['language'], direction: this.rtlService.getDirection(req['language']), };
-
Use accessibility decorators:
@ScreenReaderOptimized() @WCAGCompliance('AA') @KeyboardNavigable()
-
Provide ARIA attributes:
const ariaAttrs = this.accessibilityService.buildAriaAttributes({ role: AriaRole.DIALOG, label: 'Confirmation', });
-
Support language detection:
const language = this.i18nService.detectLanguageFromHeader( req.headers['accept-language'], );
- ✅ Non-text content has alt text (1.1.1)
- ✅ Color contrast ≥ 4.5:1 (1.4.3)
- ✅ All functionality available via keyboard (2.1.1)
- ✅ No keyboard traps (2.1.2)
- ✅ Focus order is logical (2.4.3)
- ✅ Language identified (3.1.1)
- ✅ Error identification (3.3.1)
- ✅ Name, role, value available (4.1.2)
- ✅ Status messages conveyed (4.1.3)
Recommended testing tools:
- Automated: axe DevTools, WAVE, Lighthouse
- Manual: Screen readers (NVDA, JAWS, VoiceOver)
- Keyboard: Keyboard-only navigation
- Automated reporting: Our
/accessibility/auditendpoint
- Check the Accept-Language header is being sent
- Verify the language code format (should be ISO 639-1)
- Check if language is in the supported languages list
- Fallback defaults to English
- Ensure
dir="rtl"is set on the root element - Check for hardcoded
left/rightCSS - use logical properties instead - Use
text-align: startinstead oftext-align: left - Verify with
this.rtlService.isRTL(language)
- Ensure ARIA labels are set:
aria-labeloraria-labelledby - Check for
aria-hidden="true"hiding important content - Use
aria-livefor dynamic content - Verify semantic HTML structure
- Translation management UI
- Automatic language detection by geographic location
- Regional locale variations (e.g., en-GB, en-US)
- Pluralization rules for different languages
- Custom keyboard shortcuts per language
- Accessibility audit scheduling
- Integration with external accessibility services
- User preference storage in database
- Font size customization per language