mebjas / html5-qrcode

A cross platform HTML5 QR code reader. See end to end implementation at: https://scanapp.org
https://qrcode.minhazav.dev
Apache License 2.0
5.09k stars 980 forks source link

Messages Internationalization #132

Open croxarens opened 4 years ago

croxarens commented 4 years ago

Hi there,

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

cheweytoo commented 3 years ago

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.

mebjas commented 3 years ago

@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.

jpaoletti commented 3 years ago

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!

croxarens commented 3 years ago

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.

jpaoletti commented 3 years ago

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

faustort commented 3 years ago

@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

IlyaDiallo commented 3 years ago

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).

mebjas commented 2 years ago

@mebjas

AymaneSilini commented 2 years ago

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

mebjas commented 2 years ago

Will look into this soon.

bonino97 commented 2 years ago

Will look into this soon.

we will be very grateful. :)

mebjas commented 2 years ago

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: @.***>

IlyaDiallo commented 2 years ago

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.

AlfonsoML commented 2 years ago

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.

ROBERT-MCDOWELL commented 2 years ago

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

mebjas commented 2 years ago

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.

IlyaDiallo commented 2 years ago

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).

cheweytoo commented 2 years ago

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 commented 2 years ago

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.

1sahinomer1 commented 1 year ago

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 commented 1 year ago

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;
  }
spivurno commented 1 year ago

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

mebjas commented 1 year ago

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 minhazav@gmail.com

Re: sponsorship

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

patocardo commented 1 year ago

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);
}
alvedder commented 1 year ago

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

sanddy commented 1 year ago

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 ;)

hagenholm commented 1 year ago

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'));
});
manusaavedra commented 1 year ago
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]);
            }
        }
    }
}
Felipe-Tomazetti commented 1 week ago

@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

I can help with Portuguese if you need

Felipe-Tomazetti commented 1 week ago

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 How did you translated in the codebase? Directly inside node_modules?

croxarens commented 1 week ago

@croxarens How did you translated in the codebase? Directly inside node_modules?

Correct!

Felipe-Tomazetti commented 1 week ago

@croxarens How did you translated in the codebase? Directly inside node_modules?

Correct!

But if you update the application with npm install or yarn add, won't it be overwritten again?

And I changed inside node_modules but nothing happened :/

croxarens commented 1 week ago

Sorry, you are right. It was quite a long time ago. I wasn't really into JS so I didn't want to use NPM. At that time, the library had a compiled version, which I added to my website assets and then updated the values there. Looking at the example here, you could download the https://unpkg.com/html5-qrcode file, change what you need, and use it into your system.

And I changed inside node_modules but nothing happened :/

If you do the same inside the node_modules, I guess you'll need to recompile the code and flush the caches (browser included)

But if you update the application with npm install or yarn add, won't it be overwritten again?

Yes, I guess in that case, it would be better to add into the package.json file the version you are currently at, so that npm will not update to the next version, or just clone the project and create a local path

I hope this is helpful