Aurelia I18N: Scan html sources for new keys and update translation.json files - aurelia

Is there any tool to scan aurelia project sources (html, js) files and create (update) keys in translation.json files?
Especially I want to collect keys from HTML files that use TBindingBehavior and TValueConverter translation style.

Disclaimer: The packages suggested, are developed by my employer company.
Following are main steps involved in this process.
Generating i18n keys for the html templates, using gulp-i18n-update-localization-ids
Extract keys and values to an external resource, using gulp-i18n-extract
Manually translate the values for different languages
Compile the translations to generate locale files for different language, using gulp-i18n-compile2
Following are the minimalistic gulp tasks
const gulp = require("gulp");
const path = require("path");
const updateLocalizationIds = require('gulp-i18n-update-localization-ids');
const i18nExtract = require('gulp-i18n-extract');
const i18nCompile = require('gulp-i18n-compile2');
const src = path.resolve(__dirname, "src"),
json = path.resolve(src, "*.r.json"),
html = path.resolve(src, "*.html"),
translations = path.resolve(__dirname, "translations/i18n.json"),
locales = path.resolve(__dirname, "locales"),
i18nGlobalPrefixes = new Map();
const generateI18nKeys = function () {
return gulp.src(html)
.pipe(updateLocalizationIds({
emit: 'onChangeOnly',
ignore: [{ content: v => v.startsWith('${') && v.endsWith('}') }],
idTemplate: updateLocalizationIds.prefixFilename(i18nGlobalPrefixes),
whitelist: [
{ tagName: 'h2' },
{
tagName: 'another-custom-el',
attrs: ['some-other-value1', 'some-other-value2']
}
]
}))
.pipe(gulp.dest(src));
}
const i18nExtractOptions = {
plugIns: [
new i18nExtract.html(),
new i18nExtract.json()
],
markUpdates: true,
defaultLanguages: ['de', "fr"] // add more language here as per your need
};
const extractI18n = function () {
return gulp.src([html, json])
.pipe(i18nExtract.extract(translations, i18nExtractOptions))
.pipe(gulp.dest("."));
}
const compileOptions = {
fileName: "translation.json",
defaultLanguage: "en"
};
const compileI18n = function () {
return gulp.src(translations)
.pipe(i18nCompile(compileOptions))
.pipe(gulp.dest(locales));
}
gulp.task("i18n", gulp.series(generateI18nKeys, extractI18n, compileI18n));
What's happening here?
Let us assume that you have all the html files under src directory. You can also have some plain json files under src that act as external resources. Though it is not really needed, in this example, I have used the extension *.r.json for that (r indicates resource).
The first task generateI18nKeys generates i18n keys for the html templates. For example, it transforms the following edit.html
...
<!--edit.html-->
<h2>some text</h2>
<another-custom-el some-other-value1="value1" some-other-value2="value2"></another-custom-el>
... to the following
<!--edit.html-->
<h2 t="edit.t0">some text</h2>
<another-custom-el some-other-value1="value1" some-other-value2="value2"
t="[some-other-value1]edit.t1;[some-other-value2]edit.t2"></another-custom-el>
Use the whitelist property in the config option for this task, to mark elements and attributes for the key generation target.
In the next step, the keys and the corresponding values are extracted to a json file which looks like as follows.
{
"edit": {
"content": {
"edit.t0": {
"content": "some text",
"lastModified": "2019-05-26T16:23:42.306Z",
"needsUpdate": true,
"translations": {
"de": {
"content": "",
"lastModified": ""
},
"fr": {
"content": "",
"lastModified": ""
}
}
},
"edit.t1": {
"content": "value1",
"lastModified": "2019-05-26T16:23:42.306Z",
"needsUpdate": true,
"translations": {
"de": {
"content": "",
"lastModified": ""
},
"fr": {
"content": "",
"lastModified": ""
}
}
},
"edit.t2": {
"content": "value2",
"lastModified": "2019-05-26T16:23:42.306Z",
"needsUpdate": true,
"translations": {
"de": {
"content": "",
"lastModified": ""
},
"fr": {
"content": "",
"lastModified": ""
}
}
}
},
"src": "src\\edit.html"
}
}
Note that empty contents are generated for the localeIds, specified in the task. You can manually change this file to add translations for every language, configured.
Lastly, the compileI18n task generates files for every language from the last json, that looks something like below.
{
"edit": {
"t0": "some text",
"t1": "value1",
"t2": "value2"
}
}
Note that this file can directly be consumed by the aurelia-i18n plugin. For more details check the package specific documentation.
Hope this helps.

Related

Add commands to client language

I previously made an extension with a language support for g-code. Now I'm converting it into a language server. The problem is that my extension had some commands which I registered on the client side. When using the exact same code on the language client (the client of the language server) it does not work. Does someone have an idea why that could be?
I tried copy pasting all the dependencies etc. but with no success.
Here is the source code of the package.json of the language client:
{
"name": "lsp-sample-client",
"description": "VSCode part of a language server",
"author": "Microsoft Corporation",
"license": "MIT",
"version": "0.0.1",
"publisher": "vscode",
"engines": {
"vscode": "^1.63.0"
},
"main": "./out/extension.js",
"activationEvents": [
"onLanguage:gcode",
"onLanguage:cpl"
],
"contributes": {
"commands": [
{
"command": "lineNumberer.Renumber1",
"title": "Renumber Step 1"
},
{
"command": "lineNumberer.Renumber10",
"title": "Renumber Step 10"
},
{
"command": "lineNumberer.Renumber100",
"title": "Renumber Step 100"
},
{
"command": "lineNumberer.Renumber1000",
"title": "Renumber Step 1000"
}
],
"menus": {
"editor/context": [
{
"command": "lineNumberer.Renumber1",
"title": "Renumber Step 1"
},
{
"when": "editorLangId == gcode || editorLangId == cpl",
"command": "lineNumberer.Renumber10",
"title": "Renumber Step 10"
},
{
"when": "editorLangId == gcode || editorLangId == cpl",
"command": "lineNumberer.Renumber100",
"title": "Renumber Step 100"
},
{
"when": "editorLangId == gcode || editorLangId == cpl",
"command": "lineNumberer.Renumber1000",
"title": "Renumber Step 1000"
}
]
}
},
"dependencies": {
"vscode-languageclient": "^7.0.0"
},
"devDependencies": {
"#types/vscode": "^1.63.0",
"#vscode/test-electron": "^2.1.2"
}
}
Here is the content of the client extension.ts:
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import * as path from 'path';
import { workspace, ExtensionContext, commands } from 'vscode';
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
TransportKind
} from 'vscode-languageclient/node';
import { incrementLineNumbersBy } from './lineNumberer/lineNumberer';
let client: LanguageClient;
export function activate(context: ExtensionContext) {
const renumber1 = commands.registerCommand('lineNumberer.Renumber1', () => {
const step = 1;
incrementLineNumbersBy(step);
});
const renumber10 = commands.registerCommand('lineNumberer.Renumber10', () => {
const step = 10;
incrementLineNumbersBy(step);
});
const renumber100 = commands.registerCommand('lineNumberer.Renumber100', () => {
const step = 100;
incrementLineNumbersBy(step);
});
const renumber1000 = commands.registerCommand('lineNumberer.Renumber1000', () => {
const step = 1000;
incrementLineNumbersBy(step);
});
context.subscriptions.push(renumber1);
context.subscriptions.push(renumber10);
context.subscriptions.push(renumber100);
context.subscriptions.push(renumber1000);
// The server is implemented in node
const serverModule = context.asAbsolutePath(
path.join('server', 'out', 'server.js')
);
// If the extension is launched in debug mode then the debug server options are used
// Otherwise the run options are used
const serverOptions: ServerOptions = {
run: { module: serverModule, transport: TransportKind.ipc },
debug: {
module: serverModule,
transport: TransportKind.ipc,
}
};
// Options to control the language client
const clientOptions: LanguageClientOptions = {
// Register the server for gcode and cpl documents
documentSelector: [
{ scheme: 'file', language: 'gcode' },
{ scheme: 'file', language: 'cpl' }
],
synchronize: {
// Notify the server about file changes to '.clientrc files contained in the workspace
fileEvents: workspace.createFileSystemWatcher('**/.clientrc')
}
};
// Create the language client and start the client.
client = new LanguageClient(
'languageServerExample',
'Language Server Example',
serverOptions,
clientOptions
);
// Start the client. This will also launch the server
client.start();
}
export function deactivate(): Thenable<void> | undefined {
if (!client) {
return undefined;
}
return client.stop();
}
I managed to solve the problem myself. The problem was that you have to add the contributes commands part in the ./package.json (the outer most package.json) and not the one from the language client.

Mimic the ( Show All ) link in datatables.net

I have a situation where I want to get the full (data) from the backend as a CSV file. I have already prepared the backend for that, but normally the front-end state => (filters) is not in contact with the backend unless I send a request, so I managed to solve the problem by mimicking the process of showing all data but by a custom button and a GET request ( not an ajax request ). knowing that I am using serverSide: true in datatables.
I prepared the backend to receive a request like ( Show All ) but I want that link to be sent by custom button ( Export All ) not by the show process itself as by the picture down because showing all data is not practical at all.
This is the code for the custom button
{
text: "Export All",
action: function (e, dt, node, config) {
// get the backend file here
},
},
So, How could I send a request like the same request sent by ( Show All ) by a custom button, I prepared the server to respond by the CSV file. but I need a way to get the same link to send a get request ( not by ajax ) by the same link that Show All sends?
If you are using serverSide: true that should mean you have too much data to use the default (serverSide: false) - because the browser/DataTables cannot handle the volume. For this reason I would say you should also not try to use the browser to generate a full export - it's going to be too much data (otherwise, why did you choose to use serverSide: true?).
Instead, use a server-side export utility - not DataTables.
But if you still want to pursuse this approach, you can build a custom button which downloads the entire data set to the DataTables (in your browser) and then exports that complete data to Excel.
Full Disclosure:
The following approach is inspired by the following DataTables forum post:
Customizing the data from export buttons
The following approach requires you to have a separate REST endpoint which delivers the entire data set as a JSON response (by contrast, the standard response should only be one page of data for the actual table data display and pagination.)
How you set up this endpoint is up to you (in Laravel, in your case).
Step 1: Create a custom button:
I tested with Excel, but you can do CSV, if you prefer.
buttons: [
{
extend: 'excelHtml5', // or 'csvHtml5'
text: 'All Data to Excel', // or CSV if you prefer
exportOptions: {
customizeData: function (d) {
var exportBody = getDataToExport();
d.body.length = 0;
d.body.push.apply(d.body, exportBody);
}
}
}
],
Step 2: The export function, used by the above button:
function GetDataToExport() {
var jsonResult = $.ajax({
url: '[your_GET_EVERYTHING_url_goes_here]',
success: function (result) {},
async: false
});
var exportBody = jsonResult.responseJSON.data;
return exportBody.map(function (el) {
return Object.keys(el).map(function (key) {
return el[key]
});
});
}
In the above code, my assumption is that the JSON response has the standard DataTables object structure - so, something like:
{
"data": [
{
"id": "1",
"name": "Tiger Nixon",
"position": "System Architect",
"salary": "$320,800",
"start_date": "2011/04/25",
"office": "Edinburgh",
"extn": "5421"
},
{
"id": "2",
"name": "Garrett Winters",
"position": "Accountant",
"salary": "$170,750",
"start_date": "2011/07/25",
"office": "Tokyo",
"extn": "8422"
},
{
"id": "3",
"name": "Ashton Cox",
"position": "Junior Technical Author",
"salary": "$86,000",
"start_date": "2009/01/12",
"office": "San Francisco",
"extn": "1562"
}
]
}
So, it's an object, containing a data array.
The DataTables customizeData function is what controls writing this complete JSON to the Excel file.
Overall, your DataTables code will look something like this:
$(document).ready(function() {
$('#example').DataTable( {
serverSide: true,
dom: 'Brftip',
buttons: [
{
extend: 'excelHtml5',
text: 'All Data to Excel',
exportOptions: {
customizeData: function (d) {
var exportBody = GetDataToExport();
d.body.length = 0;
d.body.push.apply(d.body, exportBody);
}
}
}
],
ajax: {
url: "[your_SINGLE_PAGE_url_goes_here]"
},
"columns": [
{ "title": "ID", "data": "id" },
{ "title": "Name", "data": "name" },
{ "title": "Position", "data": "position" },
{ "title": "Salary", "data": "salary" },
{ "title": "Start Date", "data": "start_date" },
{ "title": "Office", "data": "office" },
{ "title": "Extn.", "data": "extn" }
]
} );
} );
function GetDataToExport() {
var jsonResult = $.ajax({
url: '[your_GET_EVERYTHING_url_goes_here]',
success: function (result) {},
async: false
});
var exportBody = jsonResult.responseJSON.data;
return exportBody.map(function (el) {
return Object.keys(el).map(function (key) {
return el[key]
});
});
}
Just to repeat my initial warning: This is probably a bad idea, if you really needed to use serverSide: true because of the volume of data you have.
Use a server-side export tool instead - I'm sure Laravel/PHP has good support for generating Excel files.

How to convert from blob URL to binary?

I'm using ImageInput component inside an iterator to upload images in my create form and it generates a structure like this:
"data": {
"items": [
{
"id": 1,
"title": "test",
"subTitle": "test",
"additionalAttributes": {
"price": "3452345"
},
"images": [
{
"src": {
"rawFile": {
"path": "test.jpg"
},
"src": "blob:https://localhost:44323/82c04494-244a-49eb-9d0e-6bca5a3469f7",
"title": "test.jpg"
},
"title": "d"
}
]
}
],
"contact": {
"firstName": "test",
"lastName": "test",
"jobTitle": "test",
"emailAddress": "test#test.com",
"phoneNumber": "23234"
},
"theme_id": 1,
"endDate": "2020-06-19T22:27:00.000Z",
"status": "2"
}
}
What I'm trying to do is sending the image to an API for saving in a folder. Blob URL is an internal object in the browser son it can't be used in the API, so I tried to convert the Blob URL into a binary and send to API.
Following the tutorial I can not get the expected result. Here is my code:
I created a new dataProvider like this:
export const PrivateEventProvider = {
create: (resource: string, params: any) => {
convertFileToBase64(params.data.items[0].images[0].src.src).then(
transformedPicture => {
console.log(`transformedPicture: ${transformedPicture}`);
}
);
const convertFileToBase64 = (file: { rawFile: Blob }) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file.rawFile);
});
And I have this error
Unhandled Rejection (TypeError): Failed to execute 'readAsDataURL' on
'FileReader': parameter 1 is not of type 'Blob'.
enter image description here
So my question is, which is the correct way of uploading images to a folder using react-admin?

How to update the Strapi GraphQL cache, after creating new data?

How to update the cache, after creating new data?
Error message from Apollo
Store error: the application attempted to write an object with no provided id but the store already contains an id of UsersPermissionsUser:1 for this object. The selectionSet that was trying to be written is:
{
"kind": "Field",
"name": { "kind": "Name", "value": "user" },
"arguments": [],
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{ "kind": "Field", "name": { "kind": "Name", "value": "username" }, "arguments": [], "directives": [] },
{ "kind": "Field", "name": { "kind": "Name", "value": "__typename" } }
]
}
}
Nativescript-vue Front-end Details
1- Watch Book Mobile app in action on YouTube: https://youtu.be/sBM-ErjXWuw
2- Watch Question video for details on YouTube: https://youtu.be/wqvrcBRQpZg
{N}-vue AddBook.vue file
apolloClient
.mutate({
// Query
mutation: mutations.CREATE_BOOK,
// Parameters
variables: {
name: this.book.name,
year: this.book.year,
},
// HOW TO UPDATE
update: (store, { data }) => {
console.log("data ::::>> ", data.createBook.book);
const bookQuery = {
query: queries.ALL_BOOKS,
};
// TypeScript detail: instead of creating an interface
// I used any type access books property without compile errors.
const bookData:any = store.readQuery(bookQuery);
console.log('bookData :>> ', bookData);
// I pin-pointed data objects
// Instead of push(createBook) I've pushed data.createBook.book
bookData.books.push(data.createBook.book);
store.writeQuery({ ...bookQuery, data: bookData })
},
})
.then((data) => {
// I can even see ID in Result
console.log("new data.data id ::::: :>> ", data.data.createBook.book.id);
this.$navigateTo(App);
})
.catch((error) => {
// Error
console.error(error);
});
What are these "Book:9": { lines in the cache?
console.log store turns out:
"Book:9": {
"id": "9",
"name": "Hadi",
"year": "255",
"__typename": "Book"
},
"$ROOT_MUTATION.createBook({\"input\":{\"data\":{\"name\":\"Hadi\",\"year\":\"255\"}}})": {
You can see all front-end GitHub repo here
Download Android apk file
Our goal is to update the cache. Add Book Method is in here:
https://github.com/kaanguru/mutate-question/blob/c199f8dcc8e80e83abdbcde4811770b766befcb5/nativescript-vue/app/components/AddBook.vue#L39
Back-end details
However, this is a frontend question a running Strapi GraphQL Server is here: https://polar-badlands-01357.herokuapp.com/admin/
GraphQL Playground
USER: admin
PASSWORD: passw123
You can see GraphQL documentation
I have so much simple Strapi GrapQL Scheme:
If you want to test it using postman or insomnia you can use;
POST GraphQL Query URL: https://polar-badlands-01357.herokuapp.com/graphql
Bearer Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNTkwODI3MzE0LCJleHAiOjE1OTM0MTkzMTR9.WIK-f4dkwVAyIlP20v1PFoflpwGmRYgRrsQiRFgGdqg
NOTE: Don't get confused with $navigateTo() it's just a custom method of nativescript-vue.
It turns out;
all code was correct accept bookData.push(createBook);
// HOW TO UPDATE
update: (store, { data }) => {
console.log("data ::::>> ", data.createBook.book);
const bookQuery = {
query: queries.ALL_BOOKS,
};
// TypeScript detail: instead of creating an interface
// I used any type access books property without compile errors.
const bookData:any = store.readQuery(bookQuery);
console.log('bookData :>> ', bookData);
// I pin-pointed data objects
// Instead of push(createBook) I've pushed data.createBook.book
bookData.books.push(data.createBook.book);
store.writeQuery({ ...bookQuery, data: bookData })
},
})
Typescipt was helping
The point is; I shouldn't trust TypeScript errors, or at least I should read more about what it really says.
Typescript just asked me to be more specific while saying: Property 'push' does not exist on type 'unknown'
TypeScript was trying to tell me I need to be more specific while calling ROOT_MUTATION data. It said: Cannot find name 'createBook' But again I ignored it.
Solution Github Branch
https://github.com/kaanguru/mutate-question/tree/solution
Sources
how to update cache
Create interface for object Typescript

"Requested device not found" when using chrome.tabCapture.capture

Problem
I want to capture the audio output of a tab automatically. I'm currently thinking of doing this using Puppeteer (headful), by loading an extension that uses chrome.tabCapture.capture. From my Puppeteer script, I evaluate code within the extensions background.js to get the tab capture started. However, chrome.runtime.lastError.message is set to Requested device not found.
The extension works as expected outside of Puppeteer and in a Chrome browser.
Any idea why I'm getting Requested device not found?
What does the extension's background.js look like?
function startRecording() {
chrome.tabCapture.capture(options, stream => {
if (stream === null) {
console.log(`Last Error: ${chrome.runtime.lastError.message}`);
return;
}
try {
const recorder = new MediaRecorder(stream);
} catch (err) {
console.log(err.message);
}
recorder.addEventListener('dataavailable', event => {
const { data: blob, timecode } = event;
console.log(`${timecode}: ${blob}`);
});
const timeslice = 60 * 1000;
recorder.start(timeslice);
});
}
What does the relevant part of your Puppeteer script look like?
...
const targets = await browser.targets();
const backgroundPageTarget = targets.find(target => target.type() === 'background_page' && target.url().startsWith('chrome-extension://abcde/'));
const backgroundPage = await backgroundPageTarget.page();
const test = await backgroundPage.evaluate(() => {
startRecording();
return Promise.resolve(42);
});
...
Extension Manifest:
{
"name": "Test",
"description": "",
"version": "1.0",
"icons": {
"128": "icon.png"
},
"manifest_version": 2,
"browser_action": {
"default_popup": "test.html"
},
"background": {
"scripts": [
"background.js"
],
"persistent": true
},
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"all_frames": false,
"js": [
"contentScript.js"
]
}
],
"permissions": [
"activeTab",
"tabs",
"tabCapture",
"storage"
]
}