html5-qrcode icon indicating copy to clipboard operation
html5-qrcode copied to clipboard

Messages Internationalization

Open croxarens opened this issue 3 years ago • 9 comments

Hi there,

is there any way I can translate the messages like "Request Camera Permissions", "Scan an Image File"?

croxarens avatar Oct 29 '20 20:10 croxarens

Can I help?

https://github.com/mebjas/html5-qrcode/blob/master/src/strings.ts already separates the UI strings (and considers internationalization a TODO :-), so this looks well prepared for.

@mebjas: Have you already thought about how you'd want the internationalization to be triggered/configured? Via an additional config option maybe?

It would of course be nice to just automatically match the first language in navigator.languages that is supported by html5-qrcode's translations, but I don't know if this can be relied on to be available in all environments supported by html5-qrcode.

cheweytoo avatar Aug 09 '21 10:08 cheweytoo

@cheweytoo Thanks for the interest. This has been a key issue in my mind.

Have you already thought about how you'd want the internationalisation to be triggered/configured? Via an additional config option maybe?

I am happy to hear your thoughts on this topic. Different languages baked into the JS code with a config deciding the language is definitely one option. In case of missing language support it could fallback to English. I have not worked on projects where this is purely frontend based.

An alternative that comes to mind is generating different JS files for diff languages but that seems like a huge pain for consumers.

The design in my mind is:

| src 
......| strings.ts (become an interface only)
......| strings-factory.ts
......| strings
..................| strings.en.ts
..................| strings.fr.ts
..................| (all diff languages)

strings-factory.ts could either consume argument like this:

enum StringMode {
    ENGLISH_ONLY,  // default
    AUTO_DETECT,
    EXPLICIT
}

interface StringsConfig {
   stringMode: StringMode;
   language?: string; // only honoured when stringMode is 'EXPLICIT'.
}

And return the impl or default to english if the language is not implemented. Based on contributors we can keep adding more strings.

mebjas avatar Aug 09 '21 13:08 mebjas

Hi all, nice library !

Is there a way to manually set the texts at render or creation time ? I'd like to use Html5QrcodeScanner but I really need it translated. Some workaround comes to your mind?

Thanks!

jpaoletti avatar Sep 02 '21 18:09 jpaoletti

Hi @mebjas, I'm mostly a backend developer, and I have just general knowledge of JS. My suggestion, or at least my point of view, is to allow everyone to easily change/update/amend the language file without the need to compile something. So, for this reason, I'd stay with some basic JS options, like a file with a single object called lang and each property as a word/phrase. I think this should allow some sort of easy attribute injection too.

lang = { hello : "ciao" }

Hi @jpaoletti, I just looked for the phrase I want to translate in the codebase, and just changed it in the code. Of course this solution doesn't work with multilanguage applications.

croxarens avatar Sep 02 '21 22:09 croxarens

@croxarens Yeah I ended up doing that but it is not ideal. I agree with your proposed solution

jpaoletti avatar Sep 03 '21 01:09 jpaoletti

@cheweytoo Thanks for the interest. This has been a key issue in my mind.

Have you already thought about how you'd want the internationalisation to be triggered/configured? Via an additional config option maybe?

I am happy to hear your thoughts on this topic. Different languages baked into the JS code with a config deciding the language is definitely one option. In case of missing language support it could fallback to English. I have not worked on projects where this is purely frontend based.

An alternative that comes to mind is generating different JS files for diff languages but that seems like a huge pain for consumers.

The design in my mind is:

| src 
......| strings.ts (become an interface only)
......| strings-factory.ts
......| strings
..................| strings.en.ts
..................| strings.fr.ts
..................| (all diff languages)

strings-factory.ts could either consume argument like this:

enum StringMode {
    ENGLISH_ONLY,  // default
    AUTO_DETECT,
    EXPLICIT
}

interface StringsConfig {
   stringMode: StringMode;
   language?: string; // only honoured when stringMode is 'EXPLICIT'.
}

And return the impl or default to english if the language is not implemented. Based on contributors we can keep adding more strings.

If you start the: ......| strings.ts (become an interface only) ......| strings-factory.ts

I'll be glad to help up create ..................| strings.pt-pt.ts ..................| strings.pt-br.ts ..................| strings.es.ts

Btw thanks for this amazing work @mebjas

faustort avatar Oct 23 '21 12:10 faustort

Hi @mebjas, I'm mostly a backend developer, and I have just general knowledge of JS. My suggestion, or at least my point of view, is to allow everyone to easily change/update/amend the language file without the need to compile something. So, for this reason, I'd stay with some basic JS options, like a file with a single object called lang and each property as a word/phrase. I think this should allow some sort of easy attribute injection too.

lang = { hello : "ciao" }

Yes string injection at runtime is the most versatile, no need to hardcode translation then (but it's possible to do both).

IlyaDiallo avatar Oct 23 '21 12:10 IlyaDiallo

@mebjas

  • [ ] Start an effort on this with Spanish and French
  • [ ] Write a blog post describing how to do this for any language.
  • [ ] Request contributors to add support for more languages within the library

mebjas avatar Feb 19 '22 10:02 mebjas

Hi all. I am currently working on a React scanning app. Did you find a way to efficacely change language of elements ? Thank you.

AymaneSilini avatar Jul 07 '22 10:07 AymaneSilini

Will look into this soon.

mebjas avatar Nov 17 '22 14:11 mebjas

Will look into this soon.

we will be very grateful. :)

bonino97 avatar Nov 18 '22 23:11 bonino97

One major risk is

We can get the one time effort done for all the current strings.

But thereafter it will add extra task of localisation os every string before release.

Similarly, with string injection let's say you maintain your own strings - new changes to api can break your version because of missing string.

Any ideas on how to address these issues?

On Sat, Nov 19, 2022, 07:26 Juan Cruz Lombardo Bonino < @.***> wrote:

Will look into this soon.

we will be very grateful. :)

— Reply to this email directly, view it on GitHub https://github.com/mebjas/html5-qrcode/issues/132#issuecomment-1320647841, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAW6HBKAKWCI4XMM7YM4L6TWJAGCBANCNFSM4TEGZICA . You are receiving this because you were mentioned.Message ID: @.***>

mebjas avatar Nov 19 '22 02:11 mebjas

Similarly, with string injection let's say you maintain your own strings - new changes to api can break your version because of missing string. Any ideas on how to address these issues?

API changes should not break a translated version. At worst, any missing translation should default to English.

IlyaDiallo avatar Nov 19 '22 10:11 IlyaDiallo

The system should fallback to English if there's anything missing, that way nothing breaks and new releases aren't delayed waiting for the update of every translation.
It can be done by javascript (take missing entries from the English version or augment the English version with the entries in the translation), or by copying in every translation file the new English sentences.

AlfonsoML avatar Nov 19 '22 10:11 AlfonsoML

let the developer see in the console log the missing string.....

ROBERT-MCDOWELL avatar Nov 19 '22 11:11 ROBERT-MCDOWELL

Got it, it makes sense to me.

We would still need a process to keep updating the strings and string changes to as many languages as possible.

mebjas avatar Nov 19 '22 14:11 mebjas

Got it, it makes sense to me.

We would still need a process to keep updating the strings and string changes to as many languages as possible.

The most important step is to provide a mean for the lib users to inject the translations. Including the translations in the lib itself is a maintenance burden that you may want to avoid. Also, aside of the many languages, some may want to tweak the messages to better suit their use case (more or less verbose for instance).

IlyaDiallo avatar Nov 19 '22 16:11 IlyaDiallo

Including the translations in the lib itself is a maintenance burden that you may want to avoid.

On the other hand, developers don't tend to speak all the languages. So an ability to overrule or inject own translations is fine, but having existing translations (maybe in a separate repo) would be extremely helpful. It would also avoid an awful lot of duplicated translation work.

cheweytoo avatar Nov 19 '22 18:11 cheweytoo

We would still need a process to keep updating the strings and string changes to as many languages as possible.

One way to do that is to use versioning (think versioned API endpoints): Loading an outdated translation set would give a console message. This would make developers aware of the issue, and motivate them to contribute updates.

cheweytoo avatar Nov 19 '22 19:11 cheweytoo

Hi @mebjas ,

Any chance to update https://github.com/scanapp-org/html5-qrcode-react with the language string example

I couldn't do this integration on my own, I think it would be more explanatory

Thank you

1sahinomer1 avatar Dec 08 '22 11:12 1sahinomer1

I translated it for Turkish and this is how I found a solution for now.

 #html5qr-code-full-region {
    img[alt="Info icon"] {
      display: none;
    }
  }
  #html5-qrcode-button-camera-permission {
    text-indent: -9999px;
    line-height: 0;
    margin-bottom: 10px;
  }
  #html5-qrcode-button-camera-permission:after {
    content: "Kamera izni talep et";
    text-indent: 0;
    display: block;
    line-height: initial;
  }
  #html5-qrcode-anchor-scan-type-change {
    font-size: 0;
  }
  #html5-qrcode-anchor-scan-type-change:after {
    font-size: 1rem;
    content: "Tarama tipini değiştir";
    cursor: pointer;
  }
  #html5-qrcode-button-file-selection {
    text-indent: -9999px;
    line-height: 0;
    margin-bottom: 0 !important;
  }
  #html5-qrcode-button-file-selection:after {
    content: "Dosya seç";
    text-indent: 0;
    display: block;
    line-height: initial;
  }

  #html5qr-code-full-region__dashboard_section {
    div:first-of-type {
      div:last-of-type {
        div {
          text-indent: -9999px;
          line-height: 0;
        }
        div:after {
          content: "Fotoğrafı sürükleyip bırakabilirsiniz.";
          text-indent: 0;
          display: block;
          line-height: initial;
        }
      }
    }
  }
  #html5qr-code-full-region__header_message {
    text-indent: -9999px;
    line-height: 0;
  }
  #html5qr-code-full-region__header_message:after {
    content: "Yüklenen fotoğrafta QR kod okunmuyor lütfen kırpıp yükleyiniz.";
    text-indent: 0;
    display: block;
    line-height: initial;
  }

1sahinomer1 avatar Dec 14 '22 17:12 1sahinomer1

+1 for this! Would love to to discuss sponsoring the work.

spivurno avatar Mar 17 '23 21:03 spivurno

Sounds good, that'd be helpful - this issue is pretty high up in my radar.

I would like to learn more about the use cases - please DM at [email protected]

Re: sponsorship

Checkout https://ko-fi.com/minhazav/tiers - this would definitely help make maintenance more sustainable!

mebjas avatar Mar 18 '23 01:03 mebjas

Based on the workaround of @1sahinomer1 , I developed the following dynamic code that observes the changes that the library does. It is working on Vue, and it should work on vanilla.

/**
 * This is a Workaround to translate the interface, because as of 2023-03-31,
 * Html5QrcodeScanner the library doesn't support I18N
 * Feel free to remove this piece of anti-pattern once the library can translate
 * by itself
 * 
 * Note that as there are inner interaction, text must be replaced on the fly
 */

/**
 * It observe certain selectors to overwrite with custom texts
 * @param {HTMLElement} ref
 * @returns {void}
 */
export default function scannerTranslator(ref) {
  const mappingArray = [
    { 
      selector: '#html5-qrcode-button-camera-permission',
      text: 'Solicitar permiso de cámara'
    },
    {
      selector: '#html5-qrcode-anchor-scan-type-change',
      text: 'Cambiar el tipo de escaneo'
    },
    {
      selector: '#html5-qrcode-button-file-selection',
      text: 'Seleccionar archivo'
    },
    {
      selector: '#reader__dashboard_section > div:nth-child(1) > div:nth-child(2) > div:nth-child(2)',
      text: 'Puedes arrastrar y soltar la foto'
    },
    {
      selector: '#html5qr-code-full-region__header_message',
      text: 'No se puede leer el código QR en la foto cargada. Recorta y vuelve a cargar'
    },
  ];

  // Options for the observer (which mutations to observe)
  const config = { childList: true, subtree: true };

  // Create an observer instance linked to the callback function
  const observer = new MutationObserver(function(mutationsList) {
    for(let mutation of mutationsList) {
      if (mutation.type === 'childList') {
        mappingArray.forEach((item) => {
          const element = ref.querySelector(item.selector);
          if (element && element.textContent !== item.text) {
            element.textContent = item.text;
          }
        });
      }
    }
  });

  // Start observing the target node for configured mutations
  observer.observe(ref, config);
}

patocardo avatar Mar 31 '23 12:03 patocardo

Hi! I made a PR, can you please check it?

alvedder avatar Apr 23 '23 15:04 alvedder

Bonjour, Pour ma part, du côté français, j'ai juste traduit les deux boutons ainsi dans ma vue :


<style>
          #html5-qrcode-button-camera-start {font-size:0}
          #html5-qrcode-button-camera-start::after {
            content:"Scanner";
            font-size:initial;
          }
          #html5-qrcode-button-camera-stop {font-size:0}
          #html5-qrcode-button-camera-stop::after {
            content:"Arrêter le scan";
            font-size:initial;
          }
        </style>

et cela permet d'avoir la traduction comme je souhaite au moins ;)

sanddy avatar Apr 25 '23 13:04 sanddy

English: Since the owner does not want to make any changes for the translations I do it without changing the original code. I have created my own code and it works great.

Spanish: En vista que el dueño no quiere realizar ningun cambio para las traducciones yo lo hago sin cambiar el codigo original. He creado mi propio codigo y funciona de maravilla.


function scannerTranslator() {
	const traducciones = [
		// Html5QrcodeStrings
		{original: "QR code parse error, error =", traduccion: "Error al analizar el código QR, error ="},
		{original: "Error getting userMedia, error =", traduccion: "Error al obtener userMedia, error ="},
		{original: "The device doesn't support navigator.mediaDevices , only supported cameraIdOrConfig in this case is deviceId parameter (string).", traduccion: "El dispositivo no admite navigator.mediaDevices, en este caso sólo se admite cameraIdOrConfig como parámetro deviceId (cadena)."},
		{original: "Camera streaming not supported by the browser.", traduccion: "El navegador no admite la transmisión de la cámara."},
		{original: "Unable to query supported devices, unknown error.", traduccion: "No se puede consultar los dispositivos compatibles, error desconocido."},
		{original: "Camera access is only supported in secure context like https or localhost.", traduccion: "El acceso a la cámara sólo es compatible en un contexto seguro como https o localhost."},
		{original: "Scanner paused", traduccion: "Escáner en pausa"},
	
		// Html5QrcodeScannerStrings
		{original: "Scanning", traduccion: "Escaneando"},
		{original: "Idle", traduccion: "Inactivo"},
		{original: "Error", traduccion: "Error"},
		{original: "Permission", traduccion: "Permiso"},
		{original: "No Cameras", traduccion: "Sin cámaras"},
		{original: "Last Match:", traduccion: "Última coincidencia:"},
		{original: "Code Scanner", traduccion: "Escáner de código"},
		{original: "Request Camera Permissions", traduccion: "Solicitar permisos de cámara"},
		{original: "Requesting camera permissions...", traduccion: "Solicitando permisos de cámara..."},
		{original: "No camera found", traduccion: "No se encontró ninguna cámara"},
		{original: "Stop Scanning", traduccion: "Detener escaneo"},
		{original: "Start Scanning", traduccion: "Iniciar escaneo"},
		{original: "Switch On Torch", traduccion: "Encender linterna"},
		{original: "Switch Off Torch", traduccion: "Apagar linterna"},
		{original: "Failed to turn on torch", traduccion: "Error al encender la linterna"},
		{original: "Failed to turn off torch", traduccion: "Error al apagar la linterna"},
		{original: "Launching Camera...", traduccion: "Iniciando cámara..."},
		{original: "Scan an Image File", traduccion: "Escanear un archivo de imagen"},
		{original: "Scan using camera directly", traduccion: "Escanear usando la cámara directamente"},
		{original: "Select Camera", traduccion: "Seleccionar cámara"},
		{original: "Choose Image", traduccion: "Elegir imagen"},
		{original: "Choose Another", traduccion: "Elegir otra"},
		{original: "No image choosen", traduccion: "Ninguna imagen seleccionada"},
		{original: "Anonymous Camera", traduccion: "Cámara anónima"},
		{original: "Or drop an image to scan", traduccion: "O arrastra una imagen para escanear"},
		{original: "Or drop an image to scan (other files not supported)", traduccion: "O arrastra una imagen para escanear (otros archivos no soportados)"},
		{original: "zoom", traduccion: "zoom"},
		{original: "Loading image...", traduccion: "Cargando imagen..."},
		{original: "Camera based scan", traduccion: "Escaneo basado en cámara"},
		{original: "Fule based scan", traduccion: "Escaneo basado en archivo"},

		// LibraryInfoStrings
		{original: "Powered by ", traduccion: "Desarrollado por "},
		{original: "Report issues", traduccion: "Informar de problemas"},

		// Others
		{original: "NotAllowedError: Permission denied", traduccion: "Permiso denegado para acceder a la cámara"}
	];

	// Función para traducir un texto
	function traducirTexto(texto) {
		const traduccion = traducciones.find(t => t.original === texto);
		return traduccion ? traduccion.traduccion : texto;
	}

	// Función para traducir los nodos de texto
	function traducirNodosDeTexto(nodo) {
		if (nodo.nodeType === Node.TEXT_NODE) {
			nodo.textContent = traducirTexto(nodo.textContent.trim());
		} else {
			for (let i = 0; i < nodo.childNodes.length; i++) {
				traducirNodosDeTexto(nodo.childNodes[i]);
			}
		}
	}

	// Crear el MutationObserver
	const observer = new MutationObserver((mutations) => {
		mutations.forEach((mutation) => {
			if (mutation.type === 'childList') {
				mutation.addedNodes.forEach((nodo) => {
					traducirNodosDeTexto(nodo);
				});
			}
		});
	});

	// Configurar y ejecutar el observer
	const config = {childList: true, subtree: true};
	observer.observe(document.body, config);

	// Traducir el contenido inicial
	traducirNodosDeTexto(document.body);
}

document.addEventListener('DOMContentLoaded', function () {
// Utilizando la función scannerTranslator
	scannerTranslator(document.querySelector('#qr-reader'));
});

hagenholm avatar Apr 28 '23 02:04 hagenholm

const translates = [
    { en: "QR code parse error, error =", es: "Error al analizar el código QR, error =" },
    { en: "Error getting userMedia, error =", es: "Error al obtener userMedia, error =" },
    { en: "The device doesn't support navigator.mediaDevices , only supported cameraIdOrConfig in this case is deviceId parameter (string).", es: "El dispositivo no admite navigator.mediaDevices, en este caso sólo se admite cameraIdOrConfig como parámetro deviceId (cadena)." },
    { en: "Camera streaming not supported by the browser.", es: "El navegador no admite la transmisión de la cámara." },
    { en: "Unable to query supported devices, unknown error.", es: "No se puede consultar los dispositivos compatibles, error desconocido." },
    { en: "Camera access is only supported in secure context like https or localhost.", es: "El acceso a la cámara sólo es compatible en un contexto seguro como https o localhost." },
    { en: "Scanner paused", es: "Escáner en pausa" },
    { en: "Scanning", es: "Escaneando" },
    { en: "Idle", es: "Inactivo" },
    { en: "Error", es: "Error" },
    { en: "Permission", es: "Permiso" },
    { en: "No Cameras", es: "Sin cámaras" },
    { en: "Last Match:", es: "Última coincidencia:" },
    { en: "Code Scanner", es: "Escáner de código" },
    { en: "Request Camera Permissions", es: "Solicitar permisos de cámara" },
    { en: "Requesting camera permissions...", es: "Solicitando permisos de cámara..." },
    { en: "No camera found", es: "No se encontró ninguna cámara" },
    { en: "Stop Scanning", es: "Detener escáner" },
    { en: "Start Scanning", es: "Iniciar escáner" },
    { en: "Switch On Torch", es: "Encender linterna" },
    { en: "Switch Off Torch", es: "Apagar linterna" },
    { en: "Failed to turn on torch", es: "Error al encender la linterna" },
    { en: "Failed to turn off torch", es: "Error al apagar la linterna" },
    { en: "Launching Camera...", es: "Iniciando cámara..." },
    { en: "Scan an Image File", es: "Escanear un archivo de imagen" },
    { en: "Scan using camera directly", es: "Escanear usando la cámara directamente" },
    { en: "Select Camera", es: "Seleccionar cámara" },
    { en: "Choose Image", es: "Elegir imagen" },
    { en: "Choose Another", es: "Elegir otra" },
    { en: "No image choosen", es: "Ninguna imagen seleccionada" },
    { en: "Anonymous Camera", es: "Cámara anónima" },
    { en: "Or drop an image to scan", es: "O arrastra una imagen para escanear" },
    { en: "Or drop an image to scan (other files not supported)", es: "O arrastra una imagen para escanear (otros archivos no soportados)" },
    { en: "zoom", es: "zoom" },
    { en: "Loading image...", es: "Cargando imagen..." },
    { en: "Camera based scan", es: "Escaneo basado en cámara" },
    { en: "Fule based scan", es: "Escaneo basado en archivo" },
    { en: "Powered by ", es: "Desarrollado por " },
    { en: "Report issues", es: "Informar de problemas" },
    { en: "NotAllowedError: Permission denied", es: "Permiso denegado para acceder a la cámara" }
]

export class Html5QrcodeTranslate {
    #observer = null

    constructor(elementById) {
        this.#observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach((nodo) => {
                        this.#textNodeTranslate(nodo);
                    });
                }
            });
        })

        const config = { childList: true, subtree: true };
        this.#observer.observe(document.querySelector(elementById), config);

        this.#textNodeTranslate(document.querySelector(elementById));

        return this.#observer
    }

    disconnect() {
        this.#observer !== null && this.#observer.disconnect()
    }

    #translate(texto) {
        const translate = translates.find(t => t.en === texto);
        return translate ? translate.es : texto;
    }

    #textNodeTranslate(nodo) {
        if (nodo.nodeType === Node.TEXT_NODE) {
            nodo.textContent = this.#translate(nodo.textContent.trim());
        } else {
            for (let i = 0; i < nodo.childNodes.length; i++) {
                this.#textNodeTranslate(nodo.childNodes[i]);
            }
        }
    }
}

manusaavedra avatar Aug 27 '23 23:08 manusaavedra