/* eslint-disable unicorn/no-fn-reference-in-iterator */ /* globals jQuery */ /** * Localizing script for SVG-edit UI. * @module locale * @license MIT * * @copyright 2010 Narendra Sisodya * @copyright 2010 Alexis Deveria * */ /** * Used, for example, in the ImageLibs extension, to present libraries * (with name/URL/description) in order. * @typedef {GenericArray} module:locale.LocaleArray */ /** * The string keys of the object are two-letter language codes. * @tutorial LocaleDocs * @typedef {PlainObject} module:locale.LocaleStrings */ // keyed to an array of objects with "id" and "title" or "textContent" properties /** * @typedef {PlainObject} module:locale.LocaleSelectorValue */ import {importSetGlobalDefault} from '../external/dynamic-import-polyfill/importModule.js'; const $ = jQuery; let langParam; /** * Looks for elements to localize using the supplied `obj` to indicate * on which selectors (or IDs if `ids` is set to `true`) to set its * strings (with selectors relative to the editor root element). All * keys will be translated, but for each selector, only the first item * found matching will be modified. * If the type is `content`, the selector-identified element's children * will be checked, and the first (non-empty) text (placeholder) node * found will have its text replaced. * If the type is `title`, the element's `title` * property will be set. * If the type is `aria-label`, the element's `aria-label` attribute * will be set (i.e., instructions for screen readers when there is * otherwise no visible text to be read for the function of the form * control). * @param {"content"|"title"} type * @param {module:locale.LocaleSelectorValue} obj Selectors or IDs keyed to strings * @param {boolean} ids * @returns {void} */ export const setStrings = function (type, obj, ids) { // Root element to look for element from const parent = $('#svg_editor').parent(); Object.entries(obj).forEach(([sel, val]) => { if (!val) { console.log(sel); // eslint-disable-line no-console return; // keep old text when has no translation } if (ids) { sel = '#' + sel; } const $elem = parent.find(sel); if ($elem.length) { const elem = $elem[0]; switch (type) { case 'aria-label': elem.setAttribute('aria-label', val); break; case 'content': [...elem.childNodes].some((node) => { if (node.nodeType === 3 /* Node.TEXT_NODE */ && node.textContent.trim() ) { node.textContent = val; return true; } return false; }); break; case 'title': elem.title = val; break; } } else { console.log('Missing element for localization: ' + sel); // eslint-disable-line no-console } }); }; /** * The "data" property is generally set to an an array of objects with * "id" and "title" or "textContent" properties. * @typedef {PlainObject} module:locale.AddLangExtensionLocaleData * @property {module:locale.LocaleStrings[]} data See {@tutorial LocaleDocs} */ /** * @interface module:locale.LocaleEditorInit */ /** * @function module:locale.LocaleEditorInit#addLangData * @param {string} langParam * @returns {module:locale.AddLangExtensionLocaleData} */ let editor_; /** * Sets the current editor instance (on which `addLangData`) exists. * @function init * @memberof module:locale * @param {module:locale.LocaleEditorInit} editor * @returns {void} */ export const init = (editor) => { editor_ = editor; }; /** * @typedef {PlainObject} module:locale.LangAndData * @property {string} langParam * @property {module:locale.LocaleStrings} langData */ /** * @function module:locale.readLang * @param {module:locale.LocaleStrings} langData See {@tutorial LocaleDocs} * @fires module:svgcanvas.SvgCanvas#event:ext_addLangData * @returns {Promise} Resolves to [`LangAndData`]{@link module:locale.LangAndData} */ export const readLang = async function (langData) { const more = await editor_.addLangData(langParam); $.each(more, function (i, m) { if (m.data) { langData = $.merge(langData, m.data); } }); // Old locale file, do nothing for now. if (!langData.tools) { return undefined; } const { tools, // misc, properties, config, layers, common, ui } = langData; setStrings('content', { // Todo: Add this powered by (probably by default) but with config to remove // copyrightLabel: misc.powered_by, // Currently commented out in svg-editor.html curve_segments: properties.curve_segments, fitToContent: tools.fitToContent, fit_to_all: tools.fit_to_all, fit_to_canvas: tools.fit_to_canvas, fit_to_layer_content: tools.fit_to_layer_content, fit_to_sel: tools.fit_to_sel, icon_large: config.icon_large, icon_medium: config.icon_medium, icon_small: config.icon_small, icon_xlarge: config.icon_xlarge, image_opt_embed: config.image_opt_embed, image_opt_ref: config.image_opt_ref, includedImages: config.included_images, largest_object: tools.largest_object, layersLabel: layers.layers, page: tools.page, relativeToLabel: tools.relativeTo, selLayerLabel: layers.move_elems_to, selectedPredefined: config.select_predefined, selected_objects: tools.selected_objects, smallest_object: tools.smallest_object, straight_segments: properties.straight_segments, svginfo_bg_url: config.editor_img_url + ':', svginfo_bg_note: config.editor_bg_note, svginfo_change_background: config.background, svginfo_dim: config.doc_dims, svginfo_editor_prefs: config.editor_prefs, svginfo_height: common.height, svginfo_icons: config.icon_size, svginfo_image_props: config.image_props, svginfo_lang: config.language, svginfo_title: config.doc_title, svginfo_width: common.width, tool_docprops_cancel: common.cancel, tool_docprops_save: common.ok, tool_source_cancel: common.cancel, tool_source_save: common.ok, tool_prefs_cancel: common.cancel, tool_prefs_save: common.ok, sidepanel_handle: layers.layers.split('').join(' '), tool_clear: tools.new_doc, tool_docprops: tools.docprops, tool_export: tools.export_img, tool_import: tools.import_doc, tool_open: tools.open_doc, tool_save: tools.save_doc, tool_editor_prefs: config.editor_prefs, tool_editor_homepage: tools.editor_homepage, svginfo_units_rulers: config.units_and_rulers, svginfo_rulers_onoff: config.show_rulers, svginfo_unit: config.base_unit, svginfo_grid_settings: config.grid, svginfo_snap_onoff: config.snapping_onoff, svginfo_snap_step: config.snapping_stepsize, svginfo_grid_color: config.grid_color }, true); // Context menus const opts = {}; [ 'cut', 'copy', 'paste', 'paste_in_place', 'delete', 'group', 'ungroup', 'move_front', 'move_up', 'move_down', 'move_back' ].forEach((item) => { opts['#cmenu_canvas a[href="#' + item + '"]'] = tools[item]; }); ['dupe', 'merge_down', 'merge_all'].forEach((item) => { opts['#cmenu_layers a[href="#' + item + '"]'] = layers[item]; }); opts['#cmenu_layers a[href="#delete"]'] = layers.del; setStrings('content', opts); const ariaLabels = {}; Object.entries({ tool_blur: properties.blur, tool_position: tools.align_to_page, tool_font_family: properties.font_family, zoom_panel: ui.zoom_level, stroke_linejoin: properties.linejoin_miter, stroke_linecap: properties.linecap_butt, tool_opacity: properties.opacity }).forEach(([id, value]) => { ariaLabels['#' + id + ' button'] = value; }); Object.entries({ group_opacity: properties.opacity, zoom: ui.zoom_level }).forEach(([id, value]) => { ariaLabels['#' + id] = value; }); setStrings('aria-label', ariaLabels); setStrings('title', { align_relative_to: tools.align_relative_to, circle_cx: properties.circle_cx, circle_cy: properties.circle_cy, circle_r: properties.circle_r, cornerRadiusLabel: properties.corner_radius, ellipse_cx: properties.ellipse_cx, ellipse_cy: properties.ellipse_cy, ellipse_rx: properties.ellipse_rx, ellipse_ry: properties.ellipse_ry, fill_color: properties.fill_color, font_family: properties.font_family, idLabel: properties.id, image_height: properties.image_height, image_url: properties.image_url, image_width: properties.image_width, layer_delete: layers.del, layer_down: layers.move_down, layer_new: layers.new, layer_rename: layers.rename, layer_moreopts: common.more_opts, layer_up: layers.move_up, line_x1: properties.line_x1, line_x2: properties.line_x2, line_y1: properties.line_y1, line_y2: properties.line_y2, linecap_butt: properties.linecap_butt, linecap_round: properties.linecap_round, linecap_square: properties.linecap_square, linejoin_bevel: properties.linejoin_bevel, linejoin_miter: properties.linejoin_miter, linejoin_round: properties.linejoin_round, main_icon: tools.main_menu, palette: ui.palette_info, zoom_panel: ui.zoom_level, path_node_x: properties.node_x, path_node_y: properties.node_y, rect_height_tool: properties.rect_height, rect_width_tool: properties.rect_width, seg_type: properties.seg_type, selLayerNames: layers.move_selected, selected_x: properties.pos_x, selected_y: properties.pos_y, stroke_color: properties.stroke_color, stroke_style: properties.stroke_style, stroke_width: properties.stroke_width, svginfo_title: config.doc_title, text: properties.text_contents, toggle_stroke_tools: ui.toggle_stroke_tools, tool_add_subpath: tools.add_subpath, tool_alignbottom: tools.align_bottom, tool_aligncenter: tools.align_center, tool_alignleft: tools.align_left, tool_alignmiddle: tools.align_middle, tool_alignright: tools.align_right, tool_aligntop: tools.align_top, tool_angle: properties.angle, tool_blur: properties.blur, tool_bold: properties.bold, tool_circle: tools.mode_circle, tool_clone: tools.clone, tool_clone_multi: tools.clone, tool_delete: tools.del, tool_delete_multi: tools.del, tool_ellipse: tools.mode_ellipse, tool_fhellipse: tools.mode_fhellipse, tool_fhpath: tools.mode_fhpath, tool_fhrect: tools.mode_fhrect, tool_font_size: properties.font_size, tool_group_elements: tools.group_elements, tool_make_link: tools.make_link, tool_link_url: tools.set_link_url, tool_image: tools.mode_image, tool_italic: properties.italic, tool_line: tools.mode_line, tool_move_bottom: tools.move_bottom, tool_move_top: tools.move_top, tool_node_clone: tools.node_clone, tool_node_delete: tools.node_delete, tool_node_link: tools.node_link, tool_opacity: properties.opacity, tool_openclose_path: tools.openclose_path, tool_path: tools.mode_path, tool_position: tools.align_to_page, tool_rect: tools.mode_rect, tool_redo: tools.redo, tool_reorient: tools.reorient_path, tool_select: tools.mode_select, tool_source: tools.source_save, tool_square: tools.mode_square, tool_text: tools.mode_text, tool_topath: tools.to_path, tool_undo: tools.undo, tool_ungroup: tools.ungroup, tool_wireframe: tools.wireframe_mode, tool_zoom: tools.mode_zoom, url_notice: tools.no_embed }, true); return {langParam, langData}; }; /** * * @function module:locale.putLocale * @param {string} givenParam * @param {string[]} goodLangs * @param {{langPath: string}} conf * @fires module:svgcanvas.SvgCanvas#event:ext_addLangData * @fires module:svgcanvas.SvgCanvas#event:ext_langReady * @fires module:svgcanvas.SvgCanvas#event:ext_langChanged * @returns {Promise} Resolves to result of {@link module:locale.readLang} */ export const putLocale = async function (givenParam, goodLangs, conf) { if (givenParam) { langParam = givenParam; } else if (navigator.userLanguage) { // Explorer langParam = navigator.userLanguage; } else if (navigator.language) { // FF, Opera, ... langParam = navigator.language; } console.log('Lang: ' + langParam); // eslint-disable-line no-console // Set to English if language is not in list of good langs if (!goodLangs.includes(langParam) && langParam !== 'test') { langParam = 'en'; } const url = conf.langPath + 'lang.' + langParam + '.js'; return readLang( // Todo: Replace this with `return import(url);` when // `import()` widely supported await importSetGlobalDefault(url, { global: 'svgEditorLang_' + langParam.replace(/-/g, '_') }) ); };