@ohif/i18n - Setup optional link to Locize translation, and script to pull and store translations

This commit is contained in:
Erik Ziegler 2019-06-29 19:00:23 +02:00
parent 8a4306d1b0
commit 1b7625affd
35 changed files with 1112 additions and 211 deletions

View File

@ -24,3 +24,6 @@ yarn-error.log*
yalc.lock
.yalc
# Note: DO NOT include this file because it includes our API key
.locize

View File

@ -1,6 +1,6 @@
{
"name": "@ohif/i18n",
"version": "0.1.1",
"version": "0.2.0",
"description": "OHIF extension for internationalization",
"author": "OHIF",
"license": "MIT",
@ -14,7 +14,8 @@
},
"scripts": {
"build": "rollup -c",
"prepublishOnly": "npm run build"
"prepublishOnly": "npm run build",
"pullTranslations": "./pullTranslations.sh"
},
"peerDependencies": {
"i18next": "^17.0.3",
@ -41,6 +42,7 @@
"i18next": "^15.1.3",
"i18next-browser-languagedetector": "^3.0.1",
"lint-staged": "^8.1.0",
"locize-cli": "^4.8.0",
"prettier": "^1.15.3",
"react": "^16.0.0",
"react-dom": "^16.0.0",
@ -80,6 +82,9 @@
"dependencies": {
"@babel/runtime": "^7.2.0",
"classnames": "^2.2.6",
"i18next-locize-backend": "^2.0.0",
"locize-editor": "^2.0.0",
"locize-lastused": "^1.1.0",
"rollup-plugin-json": "^4.0.0"
}
}

View File

@ -0,0 +1,4 @@
rm -rf src/locales/
cd src/locales/
npx locize --config-path ../../.locize download --ver latest
node ./writeLocaleIndexFiles.js

View File

@ -1,9 +1,15 @@
import i18n from 'i18next';
import Backend from 'i18next-locize-backend';
import LastUsed from 'locize-lastused';
import Editor from 'locize-editor';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
import LngDetector from 'i18next-browser-languagedetector';
import customDebug from './debugger';
import pkg from '../package.json';
import { debugMode, detectionOptions } from './config';
// Note: The index.js files inside src/locales are dynamically generated
// by the pullTranslations.sh script
import locales from './locales';
function addLocales(newLocales) {
@ -23,28 +29,106 @@ function addLocales(newLocales) {
customDebug(resourceBundle, 'info');
}
function initI18n(detection = detectionOptions) {
i18n
.use(LngDetector)
.use(initReactI18next)
.init({
resources: locales,
debug: debugMode,
keySeparator: false,
interpolation: {
escapeValue: false,
},
detection,
fallbackNS: ['Common'],
defaultNS: 'Common',
react: {
wait: true,
},
})
.then(function(t) {
i18n.T = t;
customDebug(`T function available.`, 'info');
});
// Note: Developers can add the API key to use the in-context editor.
// DO NOT commit the API key
const config = window.config.i18n || {};
const locizeOptions = {
projectId: config.LOCIZE_PROJECTID,
apiKey: config.LOCIZE_API_KEY,
referenceLng: 'en-US',
fallbacklng: 'en-US',
};
const envUseLocize = !!config.USE_LOCIZE;
const envApiKeyAvailable = !!config.LOCIZE_API_KEY;
function initI18n(
detection = detectionOptions,
useLocize = envUseLocize,
apiKeyAvailable = envApiKeyAvailable
) {
let initialized;
if (useLocize) {
customDebug(`Using Locize for translation files`, 'info');
initialized = i18n
// i18next-locize-backend
// loads translations from your project, saves new keys to it (saveMissing: true)
// https://github.com/locize/i18next-locize-backend
.use(Backend)
// locize-lastused
// sets a timestamp of last access on every translation segment on locize
// -> safely remove the ones not being touched for weeks/months
// https://github.com/locize/locize-lastused
.use(LastUsed)
// locize-editor
// InContext Editor of locize ?locize=true to show it
// https://github.com/locize/locize-editor
.use(Editor)
// detect user language
// learn more: https://github.com/i18next/i18next-browser-languageDetector
.use(LanguageDetector)
// pass the i18n instance to react-i18next.
.use(initReactI18next)
// init i18next
// for all options read: https://www.i18next.com/overview/configuration-options
.init({
fallbackLng: 'en-US',
saveMissing: apiKeyAvailable,
debug: debugMode,
keySeparator: false,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
detection,
backend: locizeOptions,
locizeLastUsed: locizeOptions,
editor: {
...locizeOptions,
onEditorSaved: async (lng, ns) => {
// reload that namespace in given language
await i18n.reloadResources(lng, ns);
// trigger an event on i18n which triggers a rerender
// based on bindI18n below in react options
i18n.emit('editorSaved');
},
},
react: {
useSuspense: false, // TODO: Was seeing weird errors without this
wait: true,
bindI18n: 'languageChanged editorSaved',
},
});
} else {
customDebug(`Using local translation files`, 'info');
initialized = i18n
// detect user language
// learn more: https://github.com/i18next/i18next-browser-languageDetector
.use(LanguageDetector)
// pass the i18n instance to react-i18next.
.use(initReactI18next)
// init i18next
// for all options read: https://www.i18next.com/overview/configuration-options
.init({
fallbackLng: 'en-US',
resources: locales,
debug: debugMode,
keySeparator: false,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
detection,
react: {
wait: true,
},
});
}
initialized.then(function(t) {
i18n.T = t;
customDebug(`T function available.`, 'info');
});
}
customDebug(`version ${pkg.version} loaded.`, 'info');

View File

@ -1,43 +1,42 @@
{
"Themes": "Themes",
"Previous": "$t(Common:Previous)",
"Next": "$t(Common:Next)",
"Play": "$t(Common:Play)",
"Stop": "$t(Common:Stop)",
"Layout": "$t(Common:Layout)",
"More": "$t(Common:More)",
"Acquired": "Acquired",
"Angle": "Angle",
"Axial": "Axial",
"Bidirectional": "Bidirectional",
"Brush": "Brush",
"CINE": "CINE",
"Cancel": "Cancel",
"Circle": "Circle",
"Clear": "Clear",
"Coronal": "Coronal",
"Crosshairs": "Crosshairs",
"Magnify": "Magnify",
"ROI Window": "ROI Window",
"Probe": "Probe",
"Ellipse": "Ellipse",
"Rectangle": "Rectangle",
"Invert": "Invert",
"Rotate Right": "Rotate Right",
"Elliptical": "Elliptical",
"Flip H": "Flip H",
"Flip V": "Flip V",
"Clear": "Clear",
"Brush": "Brush",
"Coronal": "Coronal",
"Stack Scroll": "Stack Scroll",
"Measurements": "Measurements",
"Zoom": "Zoom",
"Levels": "Levels",
"Pan": "Pan",
"Length": "Length",
"Angle": "Angle",
"Bidirectional": "Bidirectional",
"Freehand": "Freehand",
"Elliptical": "Elliptical",
"Circle": "Circle",
"Invert": "Invert",
"Layout": "$t(Common:Layout)",
"Length": "Length",
"Levels": "Levels",
"Magnify": "Magnify",
"Manual": "Manual",
"Measurements": "Measurements",
"More": "$t(Common:More)",
"Next": "$t(Common:Next)",
"Pan": "Pan",
"Play": "$t(Common:Play)",
"Previous": "$t(Common:Previous)",
"Probe": "Probe",
"ROI Window": "ROI Window",
"Rectangle": "Rectangle",
"Reset": "$t(Common:Reset)",
"CINE": "CINE",
"Acquired": "Acquired",
"Sagittal": "Sagittal",
"Axial": "Axial",
"Manual": "Manual",
"Save": "Save",
"Reset to Defaults": "$t(Common:Reset) to Defaults",
"Cancel": "Cancel"
"Rotate Right": "Rotate Right",
"Sagittal": "Sagittal",
"Save": "Save",
"Stack Scroll": "Stack Scroll",
"Stop": "$t(Common:Stop)",
"Themes": "Themes",
"Zoom": "Zoom"
}

View File

@ -1,8 +1,8 @@
{
"fps": "fps",
"Skip to first image": "Skip to first $t(Common:Image)",
"Previous image": "$t(Common:Previous) $t(Common:Image)",
"Play / Stop": "$t(Common:Play) / $t(Common:Stop)",
"Next image": "$t(Common:Play) $t(Common:Image)",
"Skip to last image": "Skip, to last $t(Common:Image)"
"Play / Stop": "$t(Common:Play) / $t(Common:Stop)",
"Previous image": "$t(Common:Previous) $t(Common:Image)",
"Skip to first image": "Skip to first $t(Common:Image)",
"Skip to last image": "Skip, to last $t(Common:Image)",
"fps": "fps"
}

View File

@ -1,10 +1,10 @@
{
"Reset": "Reset",
"Previous": "Previous",
"Next": "Next",
"Play": "Play",
"Stop": "Stop",
"Image": "Image",
"Layout": "Layout",
"More": "More",
"Image": "Image"
"Next": "Next",
"Play": "Play",
"Previous": "Previous",
"Reset": "Reset",
"Stop": "Stop"
}

View File

@ -1,8 +1,8 @@
{
"About": "About",
"Back to Viewer": "Back to Viewer",
"INVESTIGATIONAL USE ONLY": "INVESTIGATIONAL USE ONLY",
"Options": "Options",
"About": "About",
"Preferences": "Preferences",
"Study list": "Study list",
"Back to Viewer": "Back to Viewer"
"Study list": "Study list"
}

View File

@ -1,9 +1,9 @@
{
"Criteria nonconformities": "Criteria nonconformities",
"Relabel": "Relabel",
"Description": "Description",
"Delete": "Delete",
"Targets": "Targets",
"Description": "Description",
"MAX": "MAX",
"NonTargets": "NonTargets",
"MAX": "MAX"
"Relabel": "Relabel",
"Targets": "Targets"
}

View File

@ -1,6 +1,6 @@
{
"User Preferences": "User Preferences",
"Save": "$t(Buttons:Save)",
"Cancel": "$t(Buttons:Cancel)",
"Reset to Defaults": "$t(Buttons:Reset to Defaults)",
"Cancel": "$t(Buttons:Cancel)"
"Save": "$t(Buttons:Save)",
"User Preferences": "User Preferences"
}

View File

@ -5,12 +5,8 @@ import Header from './Header.json';
import MeasurementTable from './MeasurementTable.json';
import UserPreferencesModal from './UserPreferencesModal.json';
/** Sublanguages */
import en_US from './US';
import en_UK from './UK';
export default {
en: {
'en-US': {
Buttons,
CineDialog,
Common,
@ -18,6 +14,4 @@ export default {
MeasurementTable,
UserPreferencesModal,
},
...en_US,
...en_UK,
};

View File

@ -1,3 +0,0 @@
{
"About": "Info"
}

View File

@ -1,7 +0,0 @@
import Header from './Header.json';
export default {
'en-UK': {
Header,
},
};

View File

@ -1,3 +0,0 @@
{
"About": "About"
}

View File

@ -1,7 +0,0 @@
import Header from './Header.json';
export default {
'en-US': {
Header,
},
};

View File

@ -1,3 +0,0 @@
{
"INVESTIGATIONAL USE ONLY": "SOLO USO DE DESAROLLO"
}

View File

@ -1,7 +0,0 @@
import Header from './Header.json';
export default {
'es-AR': {
Header,
},
};

View File

@ -1,44 +1,42 @@
{
"Themes": "Temas",
"Previous": "$t(Common:Previous)",
"Next": "$t(Common:Next)",
"Play": "$t(Common:Play)",
"Stop": "$t(Common:Stop)",
"Layout": "$t(Common:Layout)",
"More": "$t(Common:More)",
"Acquired": "Acquired",
"Angle": "Ángulo",
"Axial": "Axial",
"Bidirectional": "Bidirectional",
"Brush": "Escoba",
"CINE": "CINE",
"Cancel": "Cancelar",
"Circle": "Circle",
"Clear": "Limpiar",
"Coronal": "Coronal",
"Crosshairs": "Cruces",
"Magnify": "Lupa",
"ROI Window": "Ventana ROI",
"Probe": "Probar",
"Ellipse": "Elipse",
"Rectangle": "Rectángulo",
"Invert": "Invertido",
"Rotate Right": "Rotar ->",
"Elliptical": "Elliptical",
"Flip H": "Espejo Hor.",
"Flip V": "Espejo Ver.",
"Clear": "Limpiar",
"Brush": "Escoba",
"Coronal": "Coronal",
"Stack Scroll": "Avance X slice",
"Measurements": "Medidas",
"Zoom": "Zoom",
"Levels": "Niveles",
"Pan": "Mover",
"Length": "Medición",
"Angle": "Ángulo",
"Bidirectional": "Bidirectional",
"Freehand": "Freehand",
"Elliptical": "Elliptical",
"Circle": "Circle",
"Reset": "$t(Common:Reset)",
"CINE": "CINE",
"Acquired": "Acquired",
"Sagittal": "Sagittal",
"Axial": "Axial",
"Invert": "Invertido",
"Layout": "$t(Common:Layout)",
"Length": "Medición",
"Levels": "Niveles",
"Magnify": "Lupa",
"Manual": "Manual",
"Save": "Guardar",
"Measurements": "Medidas",
"More": "$t(Common:More)",
"Next": "$t(Common:Next)",
"Pan": "Mover",
"Play": "$t(Common:Play)",
"Previous": "$t(Common:Previous)",
"Probe": "Probar",
"ROI Window": "Ventana ROI",
"Rectangle": "Rectángulo",
"Reset": "$t(Common:Reset)",
"Reset to Defaults": "$t(Common:reset) por defectos",
"Cancel": "Cancelar",
"2D MPR": "2D MPR",
"WWWC": "WWWC"
"Rotate Right": "Rotar ->",
"Sagittal": "Sagittal",
"Save": "Guardar",
"Stack Scroll": "Avance X slice",
"Stop": "$t(Common:Stop)",
"Themes": "Temas",
"Zoom": "Zoom"
}

View File

@ -1,8 +1,8 @@
{
"fps": "fps",
"Skip to first image": "Avanza para la primera $t(Common:Image)",
"Previous image": "$t(Common:Previous) $t(Common:Image)",
"Play / Stop": "$t(Common:Play) / $t(Common:Stop)",
"Next image": "$t(Common:Play) $t(Common:Image)",
"Skip to last image": "Pular para la ultima $t(Common:Image)"
"Play / Stop": "$t(Common:Play) / $t(Common:Stop)",
"Previous image": "$t(Common:Previous) $t(Common:Image)",
"Skip to first image": "Avanza para la primera $t(Common:Image)",
"Skip to last image": "Pular para la ultima $t(Common:Image)",
"fps": "fps"
}

12
extensions/ohif-i18n/src/locales/es/Common.json Executable file → Normal file
View File

@ -1,10 +1,10 @@
{
"Reset": "Reiniciar",
"Previous": "Anterior",
"Next": "Próximo",
"Play": "Play",
"Stop": "Stop",
"Image": "Imagen",
"Layout": "Esquema",
"More": "Más",
"Image": "Imagen"
"Next": "Próximo",
"Play": "Play",
"Previous": "Anterior",
"Reset": "Reiniciar",
"Stop": "Stop"
}

View File

@ -1,8 +1,8 @@
{
"About": "Sobre",
"Back to Viewer": "Back to Viewer",
"INVESTIGATIONAL USE ONLY": "SOLO USO DE INVESTIGACIÓN",
"Options": "Opciones",
"About": "Sobre",
"Preferences": "Preferencias",
"Study list": "Lista de estudio",
"Back to Viewer": "Back to Viewer"
"Study list": "Lista de estudio"
}

View File

@ -1,3 +0,0 @@
{
"INVESTIGATIONAL USE ONLY": "SOLO USO DE INVESTIGACIÓN"
}

View File

@ -1,7 +0,0 @@
import Header from './Header.json';
export default {
'es-MX': {
Header,
},
};

View File

@ -1,12 +1,9 @@
{
"Criteria nonconformities": "Criterios de no conformidades",
"Relabel": "Reetiquetar",
"Description": "Descripción",
"Delete": "Borrar",
"Targets": "Objetivos",
"NonTargets": "NonObjetivos",
"Description": "Descripción",
"MAX": "máximo",
"Chest Wall Posterior": "Pared pectoral posterior",
"Bone Extremity": "Extremidad ósea",
"Measurements": "Mediciones"
"NonTargets": "NonObjetivos",
"Relabel": "Reetiquetar",
"Targets": "Objetivos"
}

View File

@ -1,6 +1,6 @@
{
"User Preferences": "Preferencias de usuario",
"Save": "$t(Buttons:Save)",
"Cancel": "$t(Buttons:Cancel)",
"Reset to Defaults": "$t(Buttons:Reset to Defaults)",
"Cancel": "$t(Buttons:Cancel)"
"Save": "$t(Buttons:Save)",
"User Preferences": "Preferencias de usuario"
}

View File

@ -5,10 +5,6 @@ import Header from './Header.json';
import MeasurementTable from './MeasurementTable.json';
import UserPreferencesModal from './UserPreferencesModal.json';
/** Sublanguages */
import es_AR from './AR';
import es_MX from './MX';
export default {
es: {
Buttons,
@ -18,6 +14,4 @@ export default {
MeasurementTable,
UserPreferencesModal,
},
...es_AR,
...es_MX,
};

View File

@ -1,7 +1,9 @@
import en from './en';
import es from './es';
import en_US from './en-US/';
import es from './es/';
import nl from './nl/';
export default {
...en,
...en_US,
...es,
...nl,
};

View File

@ -0,0 +1,6 @@
{
"Circle": "Cirkel",
"More": "Meer",
"Pan": "Pan",
"Zoom": "Inzoomen"
}

View File

@ -0,0 +1,3 @@
{
"More": "Meer"
}

View File

@ -0,0 +1,7 @@
{
"About": "Over",
"INVESTIGATIONAL USE ONLY": "ALLEEN VOOR ONDERZOEK",
"Options": "Opties",
"Preferences": "Voorkeuren",
"Study list": "Studie Overzicht"
}

View File

@ -0,0 +1,11 @@
import Buttons from './Buttons.json';
import Common from './Common.json';
import Header from './Header.json';
export default {
nl: {
Buttons,
Common,
Header,
},
};

View File

@ -0,0 +1,92 @@
const path = require('path');
const fs = require('fs');
const directoryPath = path.join(__dirname, 'src', 'locales');
const { lstatSync, readdirSync } = require('fs');
const { join } = require('path');
const isDirectory = source => lstatSync(source).isDirectory();
const getDirectories = source =>
readdirSync(source)
.map(name => join(source, name))
.filter(isDirectory);
const getJSONFiles = source =>
readdirSync(source)
.filter(name => name.includes('.json'))
.map(name => join(source, name))
.filter(a => !isDirectory(a));
const directories = getDirectories(directoryPath);
function writeFile(filepath, name, content) {
fs.writeFile(path.join(filepath, name), content, err => {
if (err) throw err;
});
}
// For each language directory
const languages = [];
directories.forEach(directory => {
const language = path.basename(directory);
languages.push(language);
const name = 'index.js';
// Create one file (index.js) inside the language folder
// For each namespace
let content = '';
const files = getJSONFiles(directory);
const namespaces = files.map(file => path.basename(file, '.json'));
files.forEach(file => {
const filename = path.basename(file);
const namespace = path.basename(file, '.json');
content += `import ${namespace} from "./${filename}";\n`;
});
content += '\n';
let exportLines = `export default { \n '${language}': {\n`;
namespaces.forEach(namespace => {
exportLines += ` ${namespace},\n`;
});
exportLines += ' }\n};';
content += exportLines;
// If the file {namespace}.json is present,
// create a file to import the namespace and
// export all of the namespaces for the language
// e.g.
// import namespace from './{namespace}.json';
// export { namespace1, namespace2, ... }
writeFile(directory, name, content);
});
let fileContent = '';
const languageVariables = languages.map(language => {
const languageVariable = language.replace('-', '_');
fileContent += `import ${languageVariable} from './${language}/';\n`;
return languageVariable;
});
fileContent += '\n';
fileContent += 'export default {\n';
languageVariables.forEach(language => {
fileContent += ` ...${language},\n`;
});
fileContent += '};';
// Create one file (index.js) inside the locales folder
// which exports each of the languages
// e.g.
// import language1 from './{language1}/'
// import language2 from './{language2}/'
//
// export { language1, language2 }
writeFile(directoryPath, 'index.js', fileContent);

File diff suppressed because it is too large Load Diff

View File

@ -65,4 +65,9 @@ window.config = {
// ~ Cornerstone Tools
{ commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] },
],
i18n: {
LOCIZE_PROJECTID: 'a8da3f9a-e467-4dd6-af33-474d582a0294',
LOCIZE_API_KEY: null, // Developers can use this to do in-context editing. DO NOT COMMIT THIS KEY!
USE_LOCIZE: true
}
};

View File

@ -16,4 +16,9 @@ window.config = {
},
],
},
i18n: {
LOCIZE_PROJECTID: 'a8da3f9a-e467-4dd6-af33-474d582a0294',
LOCIZE_API_KEY: null, // Developers can use this to do in-context editing. DO NOT COMMIT THIS KEY!
USE_LOCIZE: false
}
}