Skip to content
Merged
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
144 changes: 100 additions & 44 deletions src/js/crowdin.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,109 @@
const loadScript = require('./load-script');

loadScript('https://proxy-translator.app.crowdin.net/assets/proxy-translator.js', function() {
window.proxyTranslator.init({
baseUrl: "https://app.lizardbyte.dev",
appUrl: "https://proxy-translator.app.crowdin.net",
valuesParams: "U2FsdGVkX19ClOT7gAzJBnfQPMcvqkPxFWxyPrnN7UEMztYDAOA+/W4kvMqRz2gDLv2/K3hTEtwbcq5imv5k5MUAykz0uklBcdH49kqeo1AhYpNKXdwXZNYBPXmNUm5F",
distributionBaseUrl: "https://distributions.crowdin.net",
filePath: "/app.lizardbyte.dev.json",
distribution: "0913bb75b61f0b26247ffa91bw4",
languagesData: {
"fr": {"code":"fr","name":"French","twoLettersCode":"fr"},
"es-ES": {"code":"es-ES","name":"Spanish","twoLettersCode":"es"},
"de": {"code":"de","name":"German","twoLettersCode":"de"},
"it": {"code":"it","name":"Italian","twoLettersCode":"it"},
/**
* Initializes Crowdin translation widget based on project and UI platform.
* @param {string} project - Project name ('LizardByte' or 'LizardByte-docs')
* @param {string|null} platform - UI platform ('bootstrap', 'sphinx', or null)
*/
function initCrowdIn(project = 'LizardByte', platform = 'bootstrap') {
// Input validation
if (!['LizardByte', 'LizardByte-docs'].includes(project)) {
console.error('Invalid project. Must be "LizardByte" or "LizardByte-docs"');
return;
}
if (!['bootstrap', 'sphinx', null].includes(platform)) {
console.error('Invalid UI. Must be "bootstrap", "sphinx", or null');
return;
}

loadScript('https://proxy-translator.app.crowdin.net/assets/proxy-translator.js', function() {
// Configure base settings based on project
const projectSettings = {
'LizardByte': {
baseUrl: "https://app.lizardbyte.dev",
valuesParams: "U2FsdGVkX193b3LJT2/HWNIVSb3D61klmnbJ+dvGjoY2XSu35S3gL3FRLBfiXVk4nRsFlfzaC0R7JrklvnS7Xqz5im/VrO+sGzo3LbebxNIMp8LZe28udpnJcA2I2u8B",
filePath: "/app.lizardbyte.dev.json",
distribution: "0913bb75b61f0b26247ffa91bw4",
},
'LizardByte-docs': {
baseUrl: "https://docs.lizardbyte.dev",
valuesParams: "U2FsdGVkX19eQczbrFgaLYbrEBP8is5CVpC2YSnXxH/sRjWqaBtQOsLZJbSRMepcn3D2sofzZxALb2pvT3MLmM+WG5EpWSF7CzzYsAOJ+k/FpMUJ1PZ1FQmmlKCIWyD7",
filePath: "/docs.lizardbyte.dev.json",
distribution: "fb3b3d5c18de9bc717d96b91bw4",
}
};

let languagesData = {
"bg":{"code":"bg","name":"Bulgarian","twoLettersCode":"bg"},
"de":{"code":"de","name":"German","twoLettersCode":"de"},
"en":{"code":"en","name":"English","twoLettersCode":"en"},
"en-GB":{"code":"en-GB","name":"English, United Kingdom","twoLettersCode":"en"},
"en-US":{"code":"en-US","name":"English, United States","twoLettersCode":"en"},
"es-ES":{"code":"es-ES","name":"Spanish","twoLettersCode":"es"},
"fr":{"code":"fr","name":"French","twoLettersCode":"fr"},
"it":{"code":"it","name":"Italian","twoLettersCode":"it"},
"ja":{"code":"ja","name":"Japanese","twoLettersCode":"ja"},
"ko":{"code":"ko","name":"Korean","twoLettersCode":"ko"},
"pl":{"code":"pl","name":"Polish","twoLettersCode":"pl"},
"pt-BR":{"code":"pt-BR","name":"Portuguese, Brazilian","twoLettersCode":"pt"},
"pt-PT":{"code":"pt-PT","name":"Portuguese","twoLettersCode":"pt"},
"ru": {"code":"ru","name":"Russian","twoLettersCode":"ru"},
"ru":{"code":"ru","name":"Russian","twoLettersCode":"ru"},
"sv-SE":{"code":"sv-SE","name":"Swedish","twoLettersCode":"sv"},
"tr":{"code":"tr","name":"Turkish","twoLettersCode":"tr"},
"uk":{"code":"uk","name":"Ukrainian","twoLettersCode":"uk"},
"zh-CN":{"code":"zh-CN","name":"Chinese Simplified","twoLettersCode":"zh"},
"en": {"code":"en","name":"English","twoLettersCode":"en"},
"en-US": {"code":"en-US","name":"English, United States","twoLettersCode":"en"},
"en-GB": {"code":"en-GB","name":"English, United Kingdom","twoLettersCode":"en"}
},
defaultLanguage: "en",
defaultLanguageTitle: "English",
languageDetectType: "default",
poweredBy: false,
position: "bottom-left",
submenuPosition: "top-left",
"zh-TW":{"code":"zh-TW","name":"Chinese Traditional","twoLettersCode":"zh"},
};
// sort languages by name
languagesData = Object.fromEntries(Object.entries(languagesData).sort((a, b) => a[1].name.localeCompare(b[1].name)));

// Initialize Crowdin translator
window.proxyTranslator.init({
baseUrl: projectSettings[project].baseUrl,
appUrl: "https://proxy-translator.app.crowdin.net",
valuesParams: projectSettings[project].valuesParams,
distributionBaseUrl: "https://distributions.crowdin.net",
filePath: projectSettings[project].filePath,
distribution: projectSettings[project].distribution,
distributionSeparateFiles: undefined,
languagesData: languagesData,
defaultLanguage: "en",
defaultLanguageTitle: "English",
languageDetectType: "default",
poweredBy: false,
position: "bottom-left",
submenuPosition: "top-left",
});

// Apply styling based on UI framework
if (platform === null) {
return;
}
setTimeout(() => {
const container = document.getElementById('crowdin-language-picker');
const button = document.getElementsByClassName('cr-picker-button')[0];
const menu = document.getElementsByClassName('cr-picker-submenu')[0];
const selected = document.getElementsByClassName('cr-selected')[0];

if (platform === 'bootstrap') {
button.classList.add('border-white', 'btn', 'btn-outline-light', 'bg-dark', 'text-white', 'rounded-0');
menu.classList.add('border-white', 'bg-dark', 'text-white', 'rounded-0');
selected.classList.add('text-white');
} else if (platform === 'sphinx') {
container.classList.remove('cr-position-bottom-left')
container.style.width = button.offsetWidth + 10 + 'px';
container.style.position = 'relative';
container.style.left = '10px';
container.style.bottom = '10px';

// get rst versions
const sidebar = document.getElementsByClassName('sidebar-sticky')[0];

// move button to related pages
sidebar.appendChild(container);
}
}, 500); // Short delay to ensure elements are available
});
}

// change styling of language selector button
let button = document.getElementsByClassName('cr-picker-button')[0]
button.classList.add('border-white')
button.classList.add('btn')
button.classList.add('btn-outline-light')
button.classList.add('bg-dark')
button.classList.add('text-white')
button.classList.add('rounded-0')

// change styling of language selector menu
let menu = document.getElementsByClassName('cr-picker-submenu')[0]
menu.classList.add('border-white')
menu.classList.add('bg-dark')
menu.classList.add('text-white')
menu.classList.add('rounded-0')

// change styling of selected language in menu
let selected = document.getElementsByClassName('cr-selected')[0]
selected.classList.add('text-white')
});
module.exports = initCrowdIn;
132 changes: 132 additions & 0 deletions tests/crowdin.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import {
describe,
expect,
it,
jest,
beforeEach,
afterEach
} from '@jest/globals';

// We need to mock the module BEFORE importing the module that uses it
jest.mock('../src/js/load-script', () => {
return function(url, callback) {
if (callback) setTimeout(callback, 0);
return true;
};
});

const initCrowdIn = require('../src/js/crowdin');

describe('initCrowdIn', () => {
beforeEach(() => {
// Mock DOM elements
global.document.body.innerHTML = `
<div id="crowdin-language-picker">
<div class="cr-picker-button"></div>
<div class="cr-picker-submenu"></div>
<div class="cr-selected"></div>
</div>

<!-- Sphinx sidebar -->
<div class="sidebar-sticky"></div>
`;

// Mock console.error
jest.spyOn(console, 'error').mockImplementation(() => {});

// Mock window.proxyTranslator
global.window.proxyTranslator = {
init: jest.fn()
};

// Use fake timers to control setTimeout
jest.useFakeTimers();
});

afterEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
delete global.window.proxyTranslator;
});

it('should validate project parameter', () => {
initCrowdIn('InvalidProject');
expect(console.error).toHaveBeenCalledWith('Invalid project. Must be "LizardByte" or "LizardByte-docs"');
});

it('should validate platform parameter', () => {
initCrowdIn('LizardByte', 'invalidPlatform');
expect(console.error).toHaveBeenCalledWith('Invalid UI. Must be "bootstrap", "sphinx", or null');
});

it('should initialize proxyTranslator with LizardByte settings', () => {
initCrowdIn();

// Simulate script loading
jest.runAllTimers();

expect(window.proxyTranslator.init).toHaveBeenCalledWith(
expect.objectContaining({
baseUrl: "https://app.lizardbyte.dev",
distribution: "0913bb75b61f0b26247ffa91bw4",
defaultLanguage: "en"
})
);
});

it('should initialize proxyTranslator with LizardByte-docs settings', () => {
initCrowdIn('LizardByte-docs');

// Simulate script loading
jest.runAllTimers();

expect(window.proxyTranslator.init).toHaveBeenCalledWith(
expect.objectContaining({
baseUrl: "https://docs.lizardbyte.dev",
distribution: "fb3b3d5c18de9bc717d96b91bw4",
defaultLanguage: "en"
})
);
});

it('should not apply styling when platform is null', () => {
initCrowdIn('LizardByte', null);

// Simulate script loading and run the setTimeout from UI styling
jest.runAllTimers();

// Verify that no styling was applied
const button = document.getElementsByClassName('cr-picker-button')[0];
expect(button.classList.contains('btn')).toBe(false);
});

it('should apply bootstrap styling', () => {
initCrowdIn('LizardByte', 'bootstrap');

// Simulate script loading and UI styling timeout
jest.runAllTimers();

const button = document.getElementsByClassName('cr-picker-button')[0];
const menu = document.getElementsByClassName('cr-picker-submenu')[0];
const selected = document.getElementsByClassName('cr-selected')[0];

expect(button.classList.contains('btn')).toBe(true);
expect(button.classList.contains('btn-outline-light')).toBe(true);
expect(menu.classList.contains('bg-dark')).toBe(true);
expect(selected.classList.contains('text-white')).toBe(true);
});

it('should apply sphinx styling', () => {
initCrowdIn('LizardByte', 'sphinx');

// Simulate script loading and UI styling timeout
jest.runAllTimers();

const container = document.getElementById('crowdin-language-picker');
const sidebar = document.getElementsByClassName('sidebar-sticky')[0];

expect(container.classList.contains('cr-position-bottom-left')).toBe(false);
expect(container.style.position).toBe('relative');
expect(sidebar.contains(container)).toBe(true);
});
});