UNPKG

@softvisio/core

Version:
524 lines (395 loc) • 14.6 kB
import fs from "node:fs"; import Locale from "#lib/locale"; import { PLURAL_EXPRESSIONS } from "#lib/locale/constants"; import PoFileMessage from "#lib/locale/po-file/message"; export default class PoFile { #headers = {}; #messages; #language; #nplurals; #pluralExpression; #toString; #searchPath; #singularIndex; constructor ( content ) { if ( content ) { if ( typeof content === "string" ) { this.#readPoFile( content ); } else { this.#setHeaders( content.headers ); if ( content.messages ) { this.#messages = {}; for ( const [ id, message ] of Object.entries( content.messages ) ) { this.#messages[ id ] = new PoFileMessage( this, id, message ); } } } } } // static static fromFile ( path ) { const content = fs.readFileSync( path, "utf8" ); return new this( content ); } static loadLanguageDomains ( locales, poFilesLocation, { locale, currency } = {} ) { locale ||= Locale.default; for ( const id of locales ) { const domain = new Locale( { id, currency } ); const path = poFilesLocation + "/" + domain.language + ".po"; if ( fs.existsSync( path ) ) { const poFile = PoFile.fromFile( path ); domain.add( poFile.toLocale( id ) ); } locale.domains.add( domain.id, domain ); } return locale; } // properties get messages () { return this.#messages; } get language () { return this.#language; } get nplurals () { return this.#nplurals; } get pluralExpression () { return this.#pluralExpression; } get isTranslated () { if ( this.#messages ) { for ( const message of Object.values( this.#messages ) ) { // skip disabled message if ( message.isDisabled ) continue; // message is fuzzy if ( message.isFuzzy ) return false; // message is not translated if ( !message.isTranslated ) return false; } } return true; } get searchPath () { return this.#searchPath; } get singularIndex () { this.#singularIndex ??= PLURAL_EXPRESSIONS[ this.language ]( 1 ); return this.#singularIndex; } // public addEctractedMessages ( messages ) { if ( messages instanceof PoFile ) messages = messages.messages; if ( messages ) { this.#messages ??= {}; for ( const [ id, message ] of Object.entries( messages ) ) { if ( this.#messages[ id ] ) { this.#messages[ id ].addExtractedMessage( message ); } else { this.#messages[ id ] = new PoFileMessage( this, id, message ); } } return this; } } setExtractedMessages ( poFile ) { const newMessages = poFile.toJSON().messages; if ( !newMessages && !this.#messages ) return; this.#messages ??= {}; // procress old messages for ( const message of Object.values( this.#messages ) ) { // mark old messages as disabled message.isDisabled = true; // delete message without translations if ( !message.translations?.length ) delete this.#messages[ message.id ]; } if ( newMessages ) { for ( const [ msgId, message ] of Object.entries( newMessages ) ) { if ( this.#messages[ msgId ] ) { // merge messge this.#messages[ msgId ].mergeExtractedMessage( message ); // enable message this.#messages[ msgId ].isDisabled = false; } else { this.#messages[ msgId ] = new PoFileMessage( this, msgId, message.toJSON() ); } } } this.#sort(); } toString () { this.#toString ??= this.#writePoFile(); return this.#toString; } toJSON () { return { "headers": this.#headers, "messages": this.#messages, }; } toLocale ( id ) { const locale = { id, }; if ( this.#messages ) { for ( const message of Object.values( this.#messages ) ) { // skip disabled message if ( message.isDisabled ) continue; // message is not translated if ( !message.isTranslated ) continue; locale.messages ??= {}; locale.messages[ message.id ] = {}; locale.messages[ message.id ][ "" ] = message.singularTranslation; if ( message.pluralId ) { locale.messages[ message.id ][ message.pluralId ] = message.translations; } } } return new Locale( locale ); } // private #setHeaders ( headers ) { if ( !headers ) return; this.#headers = headers; const index = {}; // index headers for ( const header in headers ) { const indexedHeader = header.toLowerCase(); index[ indexedHeader ] = header; } if ( index[ "x-search-path" ] ) { this.#searchPath = headers[ index[ "x-search-path" ] ]; } if ( index[ "language" ] ) { this.#language = headers[ index.language ]; } else { this.#language = null; } if ( index[ "plural-forms" ] ) { const pluralForms = headers[ index[ "plural-forms" ] ]; this.#nplurals = +pluralForms.match( /nplurals=(\d+);/ )?.[ 1 ]; this.#pluralExpression = pluralForms.match( /plural=([^;]+);/ )?.[ 1 ]; } else { this.#nplurals = null; this.#pluralExpression = null; } } #readPoFile ( content ) { const lines = content .split( "\n" ) .map( line => line.trim() ) .filter( line => line ); var message = this.#addMessage(); while ( lines.length ) { let line = lines.shift(); let prefix, previous; // comment if ( line.startsWith( "#" ) ) { // disabled line if ( line.startsWith( "#~" ) ) { // possible new message if ( line.startsWith( "#~ msgid " ) ) { message = this.#addMessage( message ); } message.disabled = true; if ( line.startsWith( "#~ " ) ) { prefix = "#~ "; line = line.slice( 3 ).trim(); } // disabled previous line else if ( line.startsWith( "#~| " ) ) { prefix = "#~| "; previous = true; line = line.slice( 4 ).trim(); } } // previous line else if ( line.startsWith( "#| " ) ) { prefix = "#| "; previous = true; line = line.slice( 3 ).trim(); } // other comment else { // try start new message message = this.#addMessage( message ); // reference if ( line.startsWith( "#: " ) ) { message.references ||= []; message.references.push( ...line .slice( 3 ) .split( " " ) .map( reference => reference.trim() ) .filter( reference => reference ) ); } // flags else if ( line.startsWith( "#, " ) ) { message.flags = line .slice( 3 ) .split( "," ) .map( flag => flag.trim() ) .filter( flag => flag ); } // translator comment else if ( line.startsWith( "# " ) ) { const comment = line.slice( 2 ).trim(); if ( comment ) { message.translatorComments ??= []; message.translatorComments.push( comment ); } } // extracted comment else if ( line.startsWith( "#. " ) ) { const comment = line.slice( 3 ).trim(); if ( comment ) { message.extractedComments ??= []; message.extractedComments.push( comment ); } } else { throw `Po parsing error: ${ line }`; } continue; } } // msgctxt if ( line.startsWith( "msgctxt " ) ) { const string = this.#readPoString( line.slice( 8 ).trim(), lines, prefix ); if ( string ) { if ( previous ) { message.contextPrevious = string; } else { message.context = string; } } } // msgid else if ( line.startsWith( "msgid " ) ) { // try start new message if ( !previous ) message = this.#addMessage( message ); const string = this.#readPoString( line.slice( 6 ).trim(), lines, prefix ); if ( previous ) { message.idPrevious = string; } else { message.id = string; } } // msgstr else if ( line.startsWith( "msgstr " ) ) { const string = this.#readPoString( line.slice( 7 ).trim(), lines, prefix ); if ( string ) message.translations[ 0 ] = string; } // msgid_plural else if ( line.startsWith( "msgid_plural " ) ) { const string = this.#readPoString( line.slice( 13 ).trim(), lines, prefix ); if ( string ) { if ( previous ) { message.pluralIdPrevious = string; } else { message.pluralId = string; } } } // msgstr[x] else if ( line.startsWith( "msgstr[" ) ) { const index = line.indexOf( "]" ), idx = +line.slice( 7, index ); if ( typeof idx !== "number" ) throw `Po invalid line: ${ line }`; const string = this.#readPoString( line.slice( index + 2 ).trim(), lines, prefix ); if ( string ) message.translations[ idx ] = string; } else { throw `Po parsing error: ${ line }`; } } // add last message this.#addMessage( message ); } #writePoFile () { var text = ""; text += 'msgid ""\n'; // write headers text += PoFileMessage.createPoString( "msgstr", Object.entries( this.#headers ) .map( ( [ key, value ] ) => `${ key }: ${ value }\n` ) .join( "" ) ); // write messages if ( this.#messages ) { for ( const message of Object.values( this.#messages ) ) { text += "\n" + message; } } return text; } #readPoString ( firstLine, lines, prefix ) { // dequote first line var string = firstLine.slice( 1, -1 ); while ( lines.length ) { let line = lines[ 0 ].trim(); if ( prefix ) { if ( line.startsWith( prefix ) ) { line = line.slice( prefix.length ).trim(); } else { break; } } if ( !line.startsWith( '"' ) ) break; lines.shift(); // dequote string += line.slice( 1, -1 ); } // unescape return string.replaceAll( /\\["\\nt]/g, match => { if ( match === `\\"` ) return `"`; else if ( match === "\\n" ) return "\n"; else if ( match === "\\t" ) return "\t"; else if ( match === "\\\\" ) return "\\"; else return match; } ); } #addMessage ( message ) { if ( message ) { // message has no id if ( message.id == null ) { return message; } // headers else if ( message.id === "" ) { if ( !message.translations[ 0 ] ) return; const headers = {}; for ( const line of message.translations[ 0 ].split( "\n" ) ) { const idx = line.indexOf( ":" ); if ( idx < 1 ) continue; const key = line.slice( 0, idx ).trim(), value = line.slice( idx + 1 ).trim(); headers[ key ] = value; } this.#setHeaders( headers ); } // message else { this.#messages ??= {}; this.#messages[ message.id ] = new PoFileMessage( this, message.id, message ); } } return { "id": null, "translations": [], }; } #sort () { if ( !this.#messages ) return; this.#messages = Object.fromEntries( Object.entries( this.#messages ).sort( ( a, b ) => a[ 1 ].compare( b[ 1 ] ) ) ); } }