Add localization technology to the engine.

This commit is contained in:
kovacsv 2024-01-27 08:58:13 +01:00
parent 9156b9172a
commit 449ce84bcd
16 changed files with 141 additions and 55 deletions

View File

@ -28,6 +28,13 @@ export function IsObjectEmpty (obj)
return Object.keys (obj).length === 0;
}
export function FormatString (template, ...args)
{
return template.replace (/{([0-9]+)}/g, (match, index) => {
return args[index] === undefined ? match : args[index];
});
}
export function EscapeHtmlChars (str)
{
return str.replace (/</g, '&lt;').replace (/>/g, '&gt;');

View File

@ -0,0 +1,30 @@
import { FormatString } from './core.js';
let gLocalizedStrings = null;
let gLanguageCode = null;
export function SetLocalizedStrings (localizedStrings)
{
gLocalizedStrings = localizedStrings;
}
export function SetLanguageCode (languageCode)
{
gLanguageCode = languageCode;
}
export function Loc (str)
{
if (gLocalizedStrings === null || gLanguageCode === null) {
return str;
}
if (!gLocalizedStrings[str] || !gLocalizedStrings[str][gLanguageCode]) {
return str;
}
return gLocalizedStrings[str][gLanguageCode];
}
export function FLoc (str, ...args)
{
return FormatString (Loc (str), ...args);
}

View File

@ -1,4 +1,5 @@
import { ArrayBufferToUtf8String, Utf8StringToArrayBuffer } from '../io/bufferutils.js';
import { Loc } from '../core/localization.js';
export class ExportedFile
{
@ -68,12 +69,12 @@ export class ExporterBase
GetExportedMaterialName (originalName)
{
return this.GetExportedName (originalName, 'Material');
return this.GetExportedName (originalName, Loc ('Material'));
}
GetExportedMeshName (originalName)
{
return this.GetExportedName (originalName, 'Mesh');
return this.GetExportedName (originalName, Loc ('Mesh'));
}
GetExportedName (originalName, defaultName)

View File

@ -14,6 +14,7 @@ import { TextureMap } from '../model/material.js';
import { Mesh } from '../model/mesh.js';
import { Line } from '../model/line.js';
import { ArrayToCoord3D } from '../geometry/coord3d.js';
import { Loc } from '../core/localization.js';
export class Importer3dm extends ImporterBase
{
@ -55,7 +56,7 @@ export class Importer3dm extends ImporterBase
onFinish ();
});
}).catch (() => {
this.SetError ('Failed to load rhino3dm.');
this.SetError (Loc ('Failed to load rhino3dm.'));
onFinish ();
});
} else {
@ -68,12 +69,12 @@ export class Importer3dm extends ImporterBase
{
let rhinoDoc = this.rhino.File3dm.fromByteArray (fileContent);
if (rhinoDoc === null) {
this.SetError ('Failed to read Rhino file.');
this.SetError (Loc ('Failed to read Rhino file.'));
return;
}
this.ImportRhinoDocument (rhinoDoc);
if (IsModelEmpty (this.model)) {
this.SetError ('The model doesn\'t contain any 3D meshes. Try to save the model while you are in shaded view in Rhino.');
this.SetError (Loc ('The model doesn\'t contain any 3D meshes. Try to save the model while you are in shaded view in Rhino.'));
}
}
@ -105,7 +106,7 @@ export class Importer3dm extends ImporterBase
{
let docStrings = rhinoDoc.strings ();
if (docStrings.count > 0) {
let propertyGroup = new PropertyGroup ('Document user texts');
let propertyGroup = new PropertyGroup (Loc ('Document user texts'));
for (let i = 0; i < docStrings.count; i++) {
let docString = docStrings.get (i);
propertyGroup.AddProperty (new Property (PropertyType.Text, docString[0], docString[1]));
@ -238,7 +239,7 @@ export class Importer3dm extends ImporterBase
let userStrings = rhinoAttributes.getUserStrings ();
if (userStrings.length > 0) {
let propertyGroup = new PropertyGroup ('User texts');
let propertyGroup = new PropertyGroup (Loc ('User texts'));
for (let i = 0; i < userStrings.length; i++) {
let userString = userStrings[i];
propertyGroup.AddProperty (new Property (PropertyType.Text, userString[0], userString[1]));

View File

@ -2,6 +2,7 @@ import { Direction } from '../geometry/geometry.js';
import { Model } from '../model/model.js';
import { FinalizeModel } from '../model/modelfinalization.js';
import { IsModelEmpty } from '../model/modelutils.js';
import { Loc } from '../core/localization.js';
export class ImporterBase
{
@ -51,7 +52,7 @@ export class ImporterBase
}
if (IsModelEmpty (this.model)) {
this.SetError ('The model doesn\'t contain any meshes.');
this.SetError (Loc ('The model doesn\'t contain any meshes.'));
callbacks.onError ();
callbacks.onComplete ();
return;

View File

@ -12,6 +12,7 @@ import { Transformation } from '../geometry/transformation.js';
import { ColorToMaterialConverter } from './importerutils.js';
import { Property, PropertyGroup, PropertyType } from '../model/property.js';
import { Unit } from '../model/unit.js';
import { Loc } from '../core/localization.js';
export class ImporterBim extends ImporterBase
{
@ -51,7 +52,7 @@ export class ImporterBim extends ImporterBase
try {
bimJson = JSON.parse (textContent);
} catch (err) {
this.SetError ('Failed to parse bim file.');
this.SetError (Loc ('Failed to parse bim file.'));
onFinish ();
return;
}
@ -170,9 +171,9 @@ export class ImporterBim extends ImporterBase
}
let info = source.info;
let propertyGroup = new PropertyGroup ('Info');
AddProperty (propertyGroup, 'Guid', source.guid);
AddProperty (propertyGroup, 'Type', source.type);
let propertyGroup = new PropertyGroup (Loc ('Info'));
AddProperty (propertyGroup, Loc ('Guid'), source.guid);
AddProperty (propertyGroup, Loc ('Type'), source.type);
for (let propertyName in info) {
if (Object.prototype.hasOwnProperty.call (info, propertyName)) {
if (typeof info[propertyName] === 'string') {

View File

@ -8,6 +8,7 @@ import { Node } from '../model/node.js';
import { ColorToMaterialConverter } from './importerutils.js';
import { RGBAColor } from '../model/color.js';
import { Property, PropertyGroup, PropertyType } from '../model/property.js';
import { Loc } from '../core/localization.js';
import * as fflate from 'fflate';
@ -105,7 +106,7 @@ class FreeCadDocument
return false;
}
this.properties = new PropertyGroup ('Properties');
this.properties = new PropertyGroup (Loc ('Properties'));
let documentElements = documentXml.getElementsByTagName ('Document');
for (let documentElement of documentElements) {
for (let childNode of documentElement.childNodes) {
@ -140,7 +141,7 @@ class FreeCadDocument
}
let object = this.objectData.get (name);
object.properties = new PropertyGroup ('Properties');
object.properties = new PropertyGroup (Loc ('Properties'));
for (let childNode of objectElement.childNodes) {
if (childNode.tagName === 'Properties') {
this.GetPropertiesFromElement (childNode, object.properties);
@ -322,7 +323,7 @@ export class ImporterFcstd extends ImporterBase
{
let result = this.document.Init (fileContent);
if (result === DocumentInitResult.NoDocumentXml) {
this.SetError ('No Document.xml found.');
this.SetError (Loc ('No Document.xml found.'));
onFinish ();
return;
}
@ -333,7 +334,7 @@ export class ImporterFcstd extends ImporterBase
let objectsToConvert = this.document.GetObjectListToConvert ();
if (objectsToConvert.length === 0) {
this.SetError ('No importable object found.');
this.SetError (Loc ('No importable object found.'));
onFinish ();
return;
}

View File

@ -15,6 +15,7 @@ import { Node } from '../model/node.js';
import { Property, PropertyGroup, PropertyType } from '../model/property.js';
import { Triangle } from '../model/triangle.js';
import { ImporterBase } from './importerbase.js';
import { Loc, FLoc } from '../core/localization.js';
const GltfComponentType =
{
@ -287,7 +288,7 @@ class GltfExtensions
callbacks.onSuccess ();
});
}).catch (() => {
callbacks.onError ('Failed to load draco decoder.');
callbacks.onError (Loc ('Failed to load draco decoder.'));
});
} else {
callbacks.onSuccess ();
@ -527,7 +528,7 @@ export class ImporterGltf extends ImporterBase
let textContent = ArrayBufferToUtf8String (fileContent);
let gltf = JSON.parse (textContent);
if (gltf.asset.version !== '2.0') {
this.SetError ('Invalid glTF version.');
this.SetError (Loc ('Invalid glTF version.'));
onFinish ();
return;
}
@ -545,7 +546,7 @@ export class ImporterGltf extends ImporterBase
}
}
if (buffer === null) {
this.SetError ('One of the requested buffers is missing.');
this.SetError (Loc ('One of the requested buffers is missing.'));
onFinish ();
return;
}
@ -571,19 +572,19 @@ export class ImporterGltf extends ImporterBase
let reader = new BinaryReader (fileContent, true);
let magic = reader.ReadUnsignedInteger32 ();
if (magic !== GltfConstants.GLTF_STRING) {
this.SetError ('Invalid glTF file.');
this.SetError (Loc ('Invalid glTF file.'));
onFinish ();
return;
}
let version = reader.ReadUnsignedInteger32 ();
if (version !== 2) {
this.SetError ('Invalid glTF version.');
this.SetError (Loc ('Invalid glTF version.'));
onFinish ();
return;
}
let length = reader.ReadUnsignedInteger32 ();
if (length !== reader.GetByteLength ()) {
this.SetError ('Invalid glTF file.');
this.SetError (Loc ('Invalid glTF file.'));
onFinish ();
return;
}
@ -608,7 +609,7 @@ export class ImporterGltf extends ImporterBase
{
let unsupportedExtensions = this.gltfExtensions.GetUnsupportedExtensions (gltf.extensionsRequired);
if (unsupportedExtensions.length > 0) {
this.SetError ('Unsupported extension: ' + unsupportedExtensions.join (', ') + '.');
this.SetError (FLoc ('Unsupported extension: {0}.', unsupportedExtensions.join (', ')));
onFinish ();
return;
}
@ -641,7 +642,7 @@ export class ImporterGltf extends ImporterBase
}
}
this.ImportProperties (this.model, gltf.asset, 'Asset properties');
this.ImportProperties (this.model, gltf.asset, Loc ('Asset properties'));
this.ImportScene (gltf);
}
@ -809,7 +810,7 @@ export class ImporterGltf extends ImporterBase
this.ImportPrimitive (gltf, primitive, mesh);
}
this.ImportProperties (mesh, gltfMesh.extras, 'Mesh properties');
this.ImportProperties (mesh, gltfMesh.extras, Loc ('Mesh properties'));
}
ImportPrimitive (gltf, primitive, mesh)
@ -1006,7 +1007,7 @@ export class ImporterGltf extends ImporterBase
this.ImportNode (gltf, gltfNode, rootNode);
}
this.ImportProperties (this.model, scene.extras, 'Scene properties');
this.ImportProperties (this.model, scene.extras, Loc ('Scene properties'));
}
ImportNode (gltf, gltfNode, parentNode)
@ -1058,7 +1059,7 @@ export class ImporterGltf extends ImporterBase
if (gltfNode.mesh !== undefined) {
let mesh = this.model.GetMesh (gltfNode.mesh);
this.ImportProperties (mesh, gltfNode.extras, 'Node properties');
this.ImportProperties (mesh, gltfNode.extras, Loc ('Node properties'));
node.AddMeshIndex (gltfNode.mesh);
}
}

View File

@ -9,6 +9,7 @@ import { Property, PropertyGroup, PropertyType } from '../model/property.js';
import { Triangle } from '../model/triangle.js';
import { ImporterBase } from './importerbase.js';
import { ColorToMaterialConverter } from './importerutils.js';
import { Loc, FLoc } from '../core/localization.js';
export class ImporterIfc extends ImporterBase
{
@ -50,7 +51,7 @@ export class ImporterIfc extends ImporterBase
onFinish ();
});
}).catch (() => {
this.SetError ('Failed to load web-ifc.');
this.SetError (Loc ('Failed to load web-ifc.'));
onFinish ();
});
} else {
@ -79,7 +80,7 @@ export class ImporterIfc extends ImporterBase
ImportIfcMesh (modelID, ifcMesh)
{
let mesh = new Mesh ();
mesh.SetName ('Mesh ' + ifcMesh.expressID.toString ());
mesh.SetName (FLoc ('Mesh {0}', ifcMesh.expressID.toString ()));
let vertexOffset = 0;
const ifcGeometries = ifcMesh.geometries;
@ -170,11 +171,11 @@ export class ImporterIfc extends ImporterBase
break;
case 'IfcBoolean':
case 'IfcLogical':
strValue = 'Unknown';
strValue = Loc ('Unknown');
if (property.NominalValue.value === 'T') {
strValue = 'True';
strValue = Loc ('True');
} else if (property.NominalValue.value === 'F') {
strValue = 'False';
strValue = Loc ('False');
}
elemProperty = new Property (PropertyType.Text, propertyName, strValue);
break;

View File

@ -9,6 +9,7 @@ import { Mesh } from '../model/mesh.js';
import { Triangle } from '../model/triangle.js';
import { ImporterBase } from './importerbase.js';
import { NameFromLine, ParametersFromLine, ReadLines, UpdateMaterialTransparency } from './importerutils.js';
import { Loc } from '../core/localization.js';
class ObjMeshConverter
{
@ -398,7 +399,7 @@ export class ImporterObj extends ImporterBase
let vertexIndex = this.GetRelativeIndex (parseInt (vertexParams[0], 10), this.globalVertices.length);
let meshVertexIndex = this.currentMeshConverter.AddVertex (vertexIndex, this.globalVertices);
if (meshVertexIndex === null) {
this.SetError ('Invalid vertex index.');
this.SetError (Loc ('Invalid vertex index.'));
break;
}
vertices.push (meshVertexIndex);
@ -442,7 +443,7 @@ export class ImporterObj extends ImporterBase
let v1 = this.currentMeshConverter.AddVertex (vertices[i + 1], this.globalVertices);
let v2 = this.currentMeshConverter.AddVertex (vertices[i + 2], this.globalVertices);
if (v0 === null || v1 === null || v2 === null) {
this.SetError ('Invalid vertex index.');
this.SetError (Loc ('Invalid vertex index.'));
break;
}
@ -453,7 +454,7 @@ export class ImporterObj extends ImporterBase
let c1 = this.currentMeshConverter.AddVertexColor (colors[i + 1], this.globalVertexColors);
let c2 = this.currentMeshConverter.AddVertexColor (colors[i + 2], this.globalVertexColors);
if (c0 === null || c1 === null || c2 === null) {
this.SetError ('Invalid vertex color index.');
this.SetError (Loc ('Invalid vertex color index.'));
break;
}
triangle.SetVertexColors (c0, c1, c2);
@ -464,7 +465,7 @@ export class ImporterObj extends ImporterBase
let n1 = this.currentMeshConverter.AddNormal (normals[i + 1], this.globalNormals);
let n2 = this.currentMeshConverter.AddNormal (normals[i + 2], this.globalNormals);
if (n0 === null || n1 === null || n2 === null) {
this.SetError ('Invalid normal index.');
this.SetError (Loc ('Invalid normal index.'));
break;
}
triangle.SetNormals (n0, n1, n2);
@ -475,7 +476,7 @@ export class ImporterObj extends ImporterBase
let u1 = this.currentMeshConverter.AddUV (uvs[i + 1], this.globalUvs);
let u2 = this.currentMeshConverter.AddUV (uvs[i + 2], this.globalUvs);
if (u0 === null || u1 === null || u2 === null) {
this.SetError ('Invalid uv index.');
this.SetError (Loc ('Invalid uv index.'));
break;
}
triangle.SetTextureUVs (u0, u1, u2);

View File

@ -6,6 +6,7 @@ import { ConvertThreeGeometryToMesh } from '../threejs/threeutils.js';
import { ImporterBase } from './importerbase.js';
import { ColorToMaterialConverter } from './importerutils.js';
import { Unit } from '../model/unit.js';
import { Loc } from '../core/localization.js';
export class ImporterOcct extends ImporterBase
{
@ -46,7 +47,7 @@ export class ImporterOcct extends ImporterBase
this.ImportResultJson (ev.data, onFinish);
});
this.worker.addEventListener ('error', (ev) => {
this.SetError ('Failed to load occt-import-js.');
this.SetError (Loc ('Failed to load occt-import-js.'));
onFinish ();
});

View File

@ -8,6 +8,7 @@ import { Mesh } from '../model/mesh.js';
import { Triangle } from '../model/triangle.js';
import { ImporterBase } from './importerbase.js';
import { ParametersFromLine, ReadLines, UpdateMaterialTransparency } from './importerutils.js';
import { Loc, FLoc } from '../core/localization.js';
const PlyHeaderCheckResult =
{
@ -113,11 +114,12 @@ class PlyMaterialHandler
GetMaterialIndexByColor (color)
{
let materialName = 'Color ' +
IntegerToHexString (color[0]) +
IntegerToHexString (color[1]) +
IntegerToHexString (color[2]) +
IntegerToHexString (color[3]);
let materialName = FLoc ('Color {0}{1}{2}{3}',
IntegerToHexString (color[0]),
IntegerToHexString (color[1]),
IntegerToHexString (color[2]),
IntegerToHexString (color[3])
);
if (this.colorToMaterial.has (materialName)) {
return this.colorToMaterial.get (materialName);
@ -177,11 +179,11 @@ export class ImporterPly extends ImporterBase
}
} else {
if (checkResult === PlyHeaderCheckResult.NoVertices) {
this.SetError ('The model contains no vertices.');
this.SetError (Loc ('The model contains no vertices.'));
} else if (checkResult === PlyHeaderCheckResult.NoFaces) {
this.SetError ('The model contains no faces.');
this.SetError (Loc ('The model contains no faces.'));
} else {
this.SetError ('Invalid header information.');
this.SetError (Loc ('Invalid header information.'));
}
}
onFinish ();

View File

@ -1,5 +1,6 @@
import { IsDefined, ValueOrDefault, CopyObjectAttributes, IsObjectEmpty, EscapeHtmlChars } from './core/core.js';
import { IsDefined, ValueOrDefault, CopyObjectAttributes, IsObjectEmpty, FormatString, EscapeHtmlChars } from './core/core.js';
import { EventNotifier } from './core/eventnotifier.js';
import { SetLocalizedStrings, SetLanguageCode, Loc, FLoc } from './core/localization.js';
import { TaskRunner, RunTaskAsync, RunTasks, RunTasksBatch, WaitWhile } from './core/taskrunner.js';
import { Exporter } from './export/exporter.js';
import { Exporter3dm } from './export/exporter3dm.js';
@ -80,8 +81,13 @@ export {
ValueOrDefault,
CopyObjectAttributes,
IsObjectEmpty,
FormatString,
EscapeHtmlChars,
EventNotifier,
SetLocalizedStrings,
SetLanguageCode,
Loc,
FLoc,
TaskRunner,
RunTaskAsync,
RunTasks,

View File

@ -1,5 +1,6 @@
import { EscapeHtmlChars } from '../core/core.js';
import { RGBColorToHexString } from './color.js';
import { Loc } from '../core/localization.js';
export const PropertyType =
{
@ -76,7 +77,7 @@ export function PropertyToString (property)
maximumFractionDigits: 2
});
} else if (property.type === PropertyType.Boolean) {
return property.value ? 'True' : 'False';
return property.value ? Loc ('True') : Loc ('False');
} else if (property.type === PropertyType.Percent) {
return parseInt (property.value * 100, 10).toString () + '%';
} else if (property.type === PropertyType.Color) {

View File

@ -7,6 +7,7 @@ import { ParameterConverter } from '../parameters/parameterlist.js';
import { ThreeModelLoader } from '../threejs/threemodelloader.js';
import { Viewer } from './viewer.js';
import { EnvironmentSettings } from './shadingmodel.js';
import { Loc } from '../core/localization.js';
/**
* This is the main object for embedding the viewer on a website.
@ -124,7 +125,7 @@ export class EmbeddedViewer
onLoadStart : () => {
this.canvas.style.display = 'none';
progressDiv = document.createElement ('div');
progressDiv.innerHTML = 'Loading model...';
progressDiv.innerHTML = Loc ('Loading model...');
this.parentElement.appendChild (progressDiv);
},
onFileListProgress : (current, total) => {
@ -132,10 +133,10 @@ export class EmbeddedViewer
onFileLoadProgress : (current, total) => {
},
onImportStart : () => {
progressDiv.innerHTML = 'Importing model...';
progressDiv.innerHTML = Loc ('Importing model...');
},
onVisualizationStart : () => {
progressDiv.innerHTML = 'Visualizing model...';
progressDiv.innerHTML = Loc ('Visualizing model...');
},
onModelFinished : (importResult, threeObject) => {
this.parentElement.removeChild (progressDiv);
@ -161,13 +162,13 @@ export class EmbeddedViewer
this.viewer.Render ();
},
onLoadError : (importError) => {
let message = 'Unknown error.';
let message = Loc ('Unknown error.');
if (importError.code === ImportErrorCode.NoImportableFile) {
message = 'No importable file found.';
message = Loc ('No importable file found.');
} else if (importError.code === ImportErrorCode.FailedToLoadFile) {
message = 'Failed to load file for import.';
message = Loc ('Failed to load file for import.');
} else if (importError.code === ImportErrorCode.ImportFailed) {
message = 'Failed to import model.';
message = Loc ('Failed to import model.');
}
if (importError.message !== null) {
message += ' (' + importError.message + ')';

View File

@ -67,6 +67,36 @@ describe ('Core', function () {
assert.ok (!en.HasEventListener ('third_event'));
assert.strictEqual (sumValues, 90);
});
it ('Localization', function () {
assert.strictEqual (OV.Loc ('Test'), 'Test');
assert.strictEqual (OV.FLoc ('Test {0}', '1'), 'Test 1');
OV.SetLocalizedStrings ({
'Test' : {
'hu': 'Teszt'
},
'Test {0}' : {
'hu': 'Teszt {0}'
},
'Test {0} {0}' : {
'hu': 'Teszt {0} {0}'
},
'Test {0} {0} {1}' : {
'hu': 'Teszt {0} {0} {1}'
}
});
OV.SetLanguageCode ('not_existing');
assert.strictEqual (OV.Loc ('Test'), 'Test');
OV.SetLanguageCode ('hu');
assert.strictEqual (OV.Loc ('Test'), 'Teszt');
assert.strictEqual (OV.FLoc ('Test {0}', 'a'), 'Teszt a');
assert.strictEqual (OV.FLoc ('Test {0} {0}', 'a'), 'Teszt a a');
assert.strictEqual (OV.FLoc ('Test {0} {0} {1}', 'a', 'b'), 'Teszt a a b');
OV.SetLocalizedStrings (null);
OV.SetLanguageCode (null);
});
});
}