Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions TEST-README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# i18n Test Suite Documentation

This document describes the comprehensive test suite for the internationalization (i18n) functionality.

## Quick Start

**Run all tests with a single command:**
```bash
node test.js
```

This will run the comprehensive i18n tests, providing a unified summary.

## Test Files

### 0. `test.js` ⭐ **RECOMMENDED**
A unified test runner that executes all Node.js-based tests and provides a comprehensive summary.

**Usage:**
```bash
# Run all tests
node test.js

# Show help
node test.js --help
```

**Features:**
- Runs the comprehensive i18n test suite
- Provides colored output and clear summaries
- Shows overall pass/fail status
- Reminds about browser tests

### 1. `test-i18n.js`
A Node.js-based test suite that performs static analysis and validation of the i18n implementation. This test can be run from the command line and doesn't require a browser.

**Usage:**
```bash
node test-i18n.js
```

**What it tests:**
- Translation file structure and completeness
- i18n.js module code analysis
- HTML integration with data-i18n attributes
- JavaScript integration with translation functions
- Translation key coverage
- Code quality and best practices
- Edge cases and error handling
- Accessibility and internationalization

### 2. `test-i18n-browser.html`
A browser-based test suite that tests the runtime behavior of the i18n system. This test requires opening the HTML file in a web browser.

**Usage:**
1. Open `test-i18n-browser.html` in a web browser
2. Tests will run automatically on page load
3. Use the buttons to run specific test suites

**What it tests:**
- Basic i18n module functionality
- Language switching behavior
- DOM element translation
- Error handling
- LocalStorage persistence
- Translation completeness

### 3. `test-i18n.js` (also run via `test.js`)
The comprehensive test file that verifies the i18n implementation. This test performs static analysis and checks for hardcoded text, translation coverage, and i18n integration.

**Usage:**
```bash
# Run directly
node test-i18n.js

# Or run via the unified test runner
node test.js
```

## Test Coverage

The comprehensive test suite includes **65 tests** across 8 test suites:

### Test Suite 1: Translation File Structure and Completeness (7 tests)
- Validates JSON structure
- Ensures both languages have matching keys
- Verifies all values are strings
- Checks parameter placeholder consistency

### Test Suite 2: i18n.js Module Code Analysis (18 tests)
- Verifies all exported functions exist
- Checks for proper error handling
- Validates localStorage usage
- Ensures event dispatching

### Test Suite 3: HTML Integration (14 tests)
- Verifies data-i18n attributes are used
- Checks language toggle button
- Validates all translation keys exist
- Ensures proper HTML structure

### Test Suite 4: JavaScript Integration (9 tests)
- Verifies t() function usage
- Checks translation key references
- Validates parameter substitution
- Ensures language change listeners

### Test Suite 5: Translation Key Coverage (3 tests)
- Verifies error keys are used
- Checks UI key references
- Validates parameter placeholders

### Test Suite 6: Code Quality and Best Practices (5 tests)
- Checks for hardcoded German text
- Validates translation file formatting
- Ensures no empty strings
- Verifies naming conventions

### Test Suite 7: Edge Cases and Error Handling (5 tests)
- Tests missing key handling
- Validates fallback behavior
- Checks special character handling
- Tests parameter substitution edge cases

### Test Suite 8: Accessibility and Internationalization (4 tests)
- Verifies HTML lang attribute updates
- Checks translatable aria-labels
- Validates accessibility attributes
- Ensures proper ARIA roles

## Running All Tests

**Recommended: Use the unified test runner:**
```bash
node test.js
```

**Or run tests individually:**
```bash
# Run comprehensive Node.js tests (via unified runner)
node test.js

# Run comprehensive tests directly
node test-i18n.js

# Open browser tests (requires a web server or file:// protocol)
open test-i18n-browser.html
```

## Test Results

All tests should pass. The comprehensive test suite reports:
- **65 tests passed, 0 failed** (as of last run)

## Adding New Tests

When adding new translation keys or i18n features:

1. **Add translation keys to both `en.json` and `de.json`**
2. **Update HTML with `data-i18n` attributes if needed**
3. **Use `t()` function in JavaScript for dynamic translations**
4. **Run the test suite to verify everything works**

## Troubleshooting

### Test fails: "Translation key not found"
- Ensure the key exists in both `en.json` and `de.json`
- Check that the key path uses dot notation (e.g., `"drawing.title"`)

### Test fails: "Hardcoded German text found"
- Remove any hardcoded German strings from HTML or JavaScript
- Use translation keys instead

### Browser tests don't run
- Ensure `assets/i18n.js` is accessible
- Check browser console for errors
- Verify translation JSON files are in `assets/i18n/`

## Continuous Integration

These tests can be integrated into CI/CD pipelines:

```yaml
# Example GitHub Actions workflow
- name: Run i18n tests
run: |
node test.js
```

## Notes

- The comprehensive test suite performs static analysis and doesn't require a browser
- The browser test suite tests actual runtime behavior
- Both test suites complement each other for complete coverage
- Tests are designed to catch regressions when modifying i18n code

174 changes: 174 additions & 0 deletions assets/i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/**
* Internationalization (i18n) module
* Handles language switching and translation
*/

let currentLanguage = 'en';
let translations = {};

/**
* Initialize i18n system
* @param {string} lang - Language code ('en' or 'de')
*/
async function initI18n(lang = 'en') {
currentLanguage = lang || 'en';

try {
const response = await fetch(`./assets/i18n/${currentLanguage}.json`);
if (!response.ok) {
throw new Error(`Failed to load translations for ${currentLanguage}`);
}
translations = await response.json();

// Update HTML lang attribute
document.documentElement.lang = currentLanguage;

// Update page title
document.title = translations.pageTitle || 'MNIST MLP – Inference Visualization';

// Apply translations to all elements with data-i18n attribute
applyTranslations();

// Save language preference
localStorage.setItem('preferredLanguage', currentLanguage);

return translations;
} catch (error) {
console.error('Error loading translations:', error);
// Fallback to English if translation file fails to load
if (currentLanguage !== 'en') {
return initI18n('en');
}
throw error;
}
}

/**
* Get translation for a key
* @param {string} key - Translation key (supports dot notation, e.g., "drawing.title")
* @param {object} params - Parameters to replace in translation (e.g., {digit: 5})
* @returns {string} Translated string
*/
function 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;
}
}

if (typeof value !== 'string') {
console.warn(`Translation value is not a string for key: ${key}`);
return key;
}

// Replace parameters in the format {param}
if (params && Object.keys(params).length > 0) {
return value.replace(/\{(\w+)\}/g, (match, paramKey) => {
return params[paramKey] !== undefined ? String(params[paramKey]) : match;
});
}

return value;
}

/**
* Apply translations to all elements with data-i18n attribute
*/
function applyTranslations() {
const elements = document.querySelectorAll('[data-i18n]');
elements.forEach(element => {
const key = element.getAttribute('data-i18n');
const translation = t(key);

// Handle different element types
if (element.tagName === 'INPUT' && element.type === 'text') {
element.value = translation;
} else if (element.hasAttribute('aria-label')) {
element.setAttribute('aria-label', translation);
} else if (element.hasAttribute('placeholder')) {
element.placeholder = translation;
} else if (element.hasAttribute('title')) {
element.title = translation;
} else {
element.textContent = translation;
}
});

// Handle elements with data-i18n-html for HTML content
const htmlElements = document.querySelectorAll('[data-i18n-html]');
htmlElements.forEach(element => {
const key = element.getAttribute('data-i18n-html');
const translation = t(key);
element.innerHTML = translation;
});

// Handle elements with data-i18n-aria-label for aria-label attributes
const ariaLabelElements = document.querySelectorAll('[data-i18n-aria-label]');
ariaLabelElements.forEach(element => {
const key = element.getAttribute('data-i18n-aria-label');
const translation = t(key);
element.setAttribute('aria-label', translation);
});

// Update language toggle button text
const languageToggleText = document.getElementById('languageToggleText');
if (languageToggleText) {
languageToggleText.textContent = currentLanguage.toUpperCase();
}
}

/**
* Switch language
* @param {string} lang - Language code ('en' or 'de')
*/
async function switchLanguage(lang) {
if (lang === currentLanguage) return;
await initI18n(lang);

// Trigger custom event for components that need to update
window.dispatchEvent(new CustomEvent('languageChanged', { detail: { language: lang } }));
}

/**
* Get current language
* @returns {string} Current language code
*/
function getCurrentLanguage() {
return currentLanguage;
}

// Load saved language preference on initialization
document.addEventListener('DOMContentLoaded', () => {
const savedLanguage = localStorage.getItem('preferredLanguage') || 'en';
initI18n(savedLanguage).catch(error => {
console.error('Failed to initialize i18n:', error);
});

// Setup language toggle button
const languageToggleButton = document.getElementById('languageToggleButton');
if (languageToggleButton) {
languageToggleButton.addEventListener('click', () => {
const currentLang = getCurrentLanguage();
const newLang = currentLang === 'en' ? 'de' : 'en';
switchLanguage(newLang);
});
}
});

// Export for use in other modules
if (typeof window !== 'undefined') {
window.i18n = {
init: initI18n,
t,
switchLanguage,
getCurrentLanguage,
applyTranslations
};
}

Loading