Skip to content

Commit

Permalink
Merge pull request #1589 from wordpress-mobile/fix/i18n-plurals-android
Browse files Browse the repository at this point in the history
Automate gutenberg translations followup
  • Loading branch information
Tug authored Nov 20, 2019
2 parents c6d095e + afbf2a9 commit 7e1dbd8
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 208 deletions.
72 changes: 59 additions & 13 deletions bin/po2android.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
#!/usr/bin/env node

const gettextParser = require( 'gettext-parser' ),
fs = require( 'fs' );
fs = require( 'fs' ),
crypto = require( 'crypto' );

const indent = ' ';

/**
* Encode a raw string into an Android-compatible value
*
* See: https://tekeye.uk/android/examples/android-string-resources-gotchas
* @param {string} unsafeXMLValue input string to be escaped
* @return {string} Escaped string to be copied into the XML <string></string> node
*/
function escapeResourceXML( unsafeXMLValue ) {
// See: https://tekeye.uk/android/examples/android-string-resources-gotchas
// Let's first replace XML special characters that JSON.stringify does not escape: <, > and &
// Then let's use JSON.stringify to handle pre and post spaces as well as escaping ", \, \t and \n
return JSON.stringify( unsafeXMLValue.replace( /[<>&]/g, function( character ) {
Expand All @@ -18,13 +25,47 @@ function escapeResourceXML( unsafeXMLValue ) {
} ) );
}

/**
* Generate a unique string identifier to use as the `name` property in our xml.
* Try using the string first by stripping any non-alphanumeric characters and cropping it
* Then try hashing the string and appending it to the the sanatized string
* If none of the above makes a unique ref for our string throw an error
*
* @param {string} str raw string
* @param {string} prefix Optional prefix to add to the name
* @return {string} A unique name for this string
*/
const getUniqueName = ( function() {
const names = {};
const ANDROID_MAX_NAME_LENGTH = 100;
const HASH_LENGTH = 8;
return ( str, prefix = 'gutenberg_native_' ) => {
const maxNameLength = ANDROID_MAX_NAME_LENGTH - prefix.length - HASH_LENGTH - 10; // leave some margin just in case
let name = str.replace( /\W+/g, '_' ).toLocaleLowerCase().substring( 0, maxNameLength );
// trim underscores left and right
name = name.replace( /^_+|_+$/g, '' );
// if name exists, use name + hash of the full string
if ( name in names ) {
const strHashShort = crypto.createHash( 'sha1' ).update( str ).digest( 'hex' ).substring( 0, HASH_LENGTH );
name = `${ name }_${ strHashShort }`;
}
// if name still exists
if ( name in names ) {
throw new Error( `Could not generate a unique name for string "${ str }"` );
}
names[ name ] = true;
return `${ prefix }${ name }`;
};
}() );

function po2Android( poInput ) {
const po = gettextParser.po.parse( poInput );
const translations = po.translations[ '' ];
const androidResources = Object.values( translations ).map( ( translation, id ) => {
if ( translation.msgid === '' ) {
return null;
const androidResourcesMap = Object.values( translations ).reduce( ( result, translation ) => {
if ( ! translation.msgid ) {
return result;
}
const uniqueName = getUniqueName( translation.msgid );
const escapedValue = escapeResourceXML( translation.msgid );
const escapedValuePlural = escapeResourceXML( translation.msgid_plural || '' );
const comment = translation.comments.extracted || '';
Expand All @@ -33,17 +74,22 @@ function po2Android( poInput ) {
localizedEntry += `${ indent }<!-- ${ comment.replace( '--', '—' ) } -->\n`;
}
if ( translation.msgid_plural ) {
localizedEntry += `${ indent }<plurals name="gutenberg_native_string_${ id }" tools:ignore="UnusedResources">
${ indent }${ indent }<item quantity="one">${ escapedValue }</item>
${ indent }${ indent }<item quantity="other">${ escapedValuePlural }</item>
${ indent }</plurals>
localizedEntry += `${ indent }<string-array name="${ uniqueName }" tools:ignore="UnusedResources">
${ indent }${ indent }<item>${ escapedValue }</item>
${ indent }${ indent }<item>${ escapedValuePlural }</item>
${ indent }</string-array>
`;
} else {
localizedEntry += `${ indent }<string name="gutenberg_native_string_${ id }" tools:ignore="UnusedResources">${ escapedValue }</string>\n`;
localizedEntry += `${ indent }<string name="${ uniqueName }" tools:ignore="UnusedResources">${ escapedValue }</string>\n`;
}
return localizedEntry;
} ).filter( Boolean );
return `<?xml version="1.0" encoding="utf-8"?>\n<resources xmlns:tools="http://schemas.android.com/tools">\n${ androidResources.join( '' ) }</resources>\n`;
result[ uniqueName ] = localizedEntry;
return result;
}, {} );
// try to minimize changes in diffs by sorting strings
const androidResourcesSortedList = Object.entries( androidResourcesMap )
.sort( ( left, right ) => left[ 0 ].localeCompare( right[ 0 ] ) )
.map( ( entry ) => entry[ 1 ] );
return `<?xml version="1.0" encoding="utf-8"?>\n<resources xmlns:tools="http://schemas.android.com/tools">\n${ androidResourcesSortedList.join( '' ) }</resources>\n`;
}

if ( require.main === module ) {
Expand Down
16 changes: 10 additions & 6 deletions bin/po2swift.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ const gettextParser = require( 'gettext-parser' ),
function po2Swift( poInput ) {
const po = gettextParser.po.parse( poInput );
const translations = po.translations[ '' ];
const swiftStrings = Object.values( translations ).map( ( translation, id ) => {
if ( translation.msgid === '' ) {
return null;
const swiftStringsMap = Object.values( translations ).reduce( ( result, translation ) => {
if ( ! translation.msgid ) {
return result;
}
const encodedValue = JSON.stringify( translation.msgid );
const encodedComment = JSON.stringify( translation.comments.extracted || '' );
return `let string${ id } = NSLocalizedString(${ encodedValue }, comment: ${ encodedComment })`;
} ).filter( Boolean );
return swiftStrings.join( '\n' ) + '\n';
result[ translation.msgid ] = `_ = NSLocalizedString(${ encodedValue }, comment: ${ encodedComment })`;
return result;
}, {} );
const swiftStringsSortedList = Object.entries( swiftStringsMap )
.sort( ( left, right ) => left[ 0 ].localeCompare( right[ 0 ] ) )
.map( ( entry ) => entry[ 1 ] );
return `import Foundation\n\nprivate func dummy() {\n ${ swiftStringsSortedList.join( '\n ' ) }\n`;
}

if ( require.main === module ) {
Expand Down
Loading

0 comments on commit 7e1dbd8

Please sign in to comment.