Browse Source

Setup code for changing languages

Andrew Collington 3 năm trước cách đây
mục cha
commit
5eff1f173e
5 tập tin đã thay đổi với 315 bổ sung229 xóa
  1. 100 74
      build/_frontend/interface.jsx
  2. 2 2
      build/build.php
  3. 4 2
      build/template.phps
  4. 202 148
      index.php
  5. 7 3
      src/Opcache/Service.php

+ 100 - 74
build/_frontend/interface.jsx

@@ -67,6 +67,16 @@ class Interface extends React.Component {
         return v ? !!v[2] : false;
     };
 
+    txt = (text, ...args) => {
+        if (this.props.language !== null && this.props.language.hasOwnProperty(text) && this.props.language[text]) {
+            text = this.props.language[text];
+        }
+        args.forEach((arg, i) => {
+            text = text.replaceAll(`{${i}}`, arg);
+        });
+        return text;
+    };
+
     render() {
         const { opstate, realtimeRefresh, ...otherProps } = this.props;
         return (
@@ -78,6 +88,7 @@ class Interface extends React.Component {
                         resetting={this.state.resetting}
                         realtimeHandler={this.realtimeHandler}
                         resetHandler={this.resetHandler}
+                        txt={this.txt}
                     />
                 </header>
                 <Footer version={this.props.opstate.version.gui} />
@@ -91,29 +102,33 @@ function MainNavigation(props) {
     return (
         <nav className="main-nav">
             <Tabs>
-                <div label="Overview" tabId="overview" tabIndex={1}>
+                <div label={props.txt("Overview")} tabId="overview" tabIndex={1}>
                     <OverviewCounts
                         overview={props.opstate.overview}
                         highlight={props.highlight}
                         useCharts={props.useCharts}
+                        txt={props.txt}
                     />
                     <div id="info" className="tab-content-overview-info">
                         <GeneralInfo
                             start={props.opstate.overview && props.opstate.overview.readable.start_time || null}
                             reset={props.opstate.overview && props.opstate.overview.readable.last_restart_time || null}
                             version={props.opstate.version}
+                            txt={props.txt}
                         />
                         <Directives
                             directives={props.opstate.directives}
+                            txt={props.txt}
                         />
                         <Functions
                             functions={props.opstate.functions}
+                            txt={props.txt}
                         />
                     </div>
                 </div>
                 {
                     props.allow.filelist &&
-                        <div label="Cached" tabId="cached" tabIndex={2}>
+                        <div label={props.txt("Cached")} tabId="cached" tabIndex={2}>
                             <CachedFiles
                                 perPageLimit={props.perPageLimit}
                                 allFiles={props.opstate.files}
@@ -121,32 +136,35 @@ function MainNavigation(props) {
                                 debounceRate={props.debounceRate}
                                 allow={{fileList: props.allow.filelist, invalidate: props.allow.invalidate}}
                                 realtime={props.realtime}
+                                txt={props.txt}
                             />
                         </div>
                 }
                 {
                     (props.allow.filelist && props.opstate.blacklist.length &&
-                        <div label="Ignored" tabId="ignored" tabIndex={3}>
+                        <div label={props.txt("Ignored")} tabId="ignored" tabIndex={3}>
                             <IgnoredFiles
                                 perPageLimit={props.perPageLimit}
                                 allFiles={props.opstate.blacklist}
                                 allow={{fileList: props.allow.filelist }}
+                                txt={props.txt}
                             />
                         </div>)
                 }
                 {
                     (props.allow.filelist && props.opstate.preload.length &&
-                        <div label="Preloaded" tabId="preloaded" tabIndex={4}>
+                        <div label={props.txt("Preloaded")} tabId="preloaded" tabIndex={4}>
                             <PreloadedFiles
                                 perPageLimit={props.perPageLimit}
                                 allFiles={props.opstate.preload}
                                 allow={{fileList: props.allow.filelist }}
+                                txt={props.txt}
                             />
                         </div>)
                 }
                 {
                     props.allow.reset &&
-                        <div label="Reset cache" tabId="resetCache"
+                        <div label={props.txt("Reset cache")} tabId="resetCache"
                            className={`nav-tab-link-reset${props.resetting ? ' is-resetting pulse' : ''}`}
                            handler={props.resetHandler}
                            tabIndex={5}
@@ -154,7 +172,7 @@ function MainNavigation(props) {
                 }
                 {
                     props.allow.realtime &&
-                        <div label={`${props.realtime ? 'Disable' : 'Enable'} real-time update`} tabId="toggleRealtime"
+                        <div label={props.txt(`${props.realtime ? 'Disable' : 'Enable'} real-time update`)} tabId="toggleRealtime"
                             className={`nav-tab-link-realtime${props.realtime ? ' live-update pulse' : ''}`}
                             handler={props.realtimeHandler}
                             tabIndex={6}
@@ -256,16 +274,16 @@ function OverviewCounts(props) {
     if (props.overview === false) {
         return (
             <p class="file-cache-only">
-                You have <i>opcache.file_cache_only</i> turned on.  As a result, the memory information is not available.  Statistics and file list may also not be returned by <i>opcache_get_statistics()</i>.
+                {props.txt(`You have <i>opcache.file_cache_only</i> turned on.  As a result, the memory information is not available.  Statistics and file list may also not be returned by <i>opcache_get_statistics()</i>.`)}
             </p>
         );
     }
 
     const graphList = [
-        {id: 'memoryUsageCanvas', title: 'memory', show: props.highlight.memory, value: props.overview.used_memory_percentage},
-        {id: 'hitRateCanvas', title: 'hit rate', show: props.highlight.hits, value: props.overview.hit_rate_percentage},
-        {id: 'keyUsageCanvas', title: 'keys', show: props.highlight.keys, value: props.overview.used_key_percentage},
-        {id: 'jitUsageCanvas', title: 'jit buffer', show: props.highlight.jit, value: props.overview.jit_buffer_used_percentage}
+        {id: 'memoryUsageCanvas', title: props.txt('memory'), show: props.highlight.memory, value: props.overview.used_memory_percentage},
+        {id: 'hitRateCanvas', title: props.txt('hit rate'), show: props.highlight.hits, value: props.overview.hit_rate_percentage},
+        {id: 'keyUsageCanvas', title: props.txt('keys'), show: props.highlight.keys, value: props.overview.used_key_percentage},
+        {id: 'jitUsageCanvas', title: props.txt('jit buffer'), show: props.highlight.jit, value: props.overview.jit_buffer_used_percentage}
     ];
 
     return (
@@ -291,6 +309,7 @@ function OverviewCounts(props) {
                 jitBuffer={props.overview.readable.jit_buffer_size || null}
                 jitBufferFree={props.overview.readable.jit_buffer_free || null}
                 jitBufferFreePercentage={props.overview.jit_buffer_used_percentage || null}
+                txt={props.txt}
             />
             <StatisticsPanel
                 num_cached_scripts={props.overview.readable.num_cached_scripts}
@@ -299,6 +318,7 @@ function OverviewCounts(props) {
                 blacklist_miss={props.overview.readable.blacklist_miss}
                 num_cached_keys={props.overview.readable.num_cached_keys}
                 max_cached_keys={props.overview.readable.max_cached_keys}
+                txt={props.txt}
             />
             {props.overview.readable.interned &&
                 <InternedStringsPanel
@@ -306,6 +326,7 @@ function OverviewCounts(props) {
                     strings_used_memory={props.overview.readable.interned.strings_used_memory}
                     strings_free_memory={props.overview.readable.interned.strings_free_memory}
                     number_of_strings={props.overview.readable.interned.number_of_strings}
+                    txt={props.txt}
                 />
             }
         </div>
@@ -317,15 +338,15 @@ function GeneralInfo(props) {
     return (
         <table className="tables general-info-table">
             <thead>
-                <tr><th colSpan="2">General info</th></tr>
+                <tr><th colSpan="2">{props.txt('General info')}</th></tr>
             </thead>
             <tbody>
                 <tr><td>Zend OPcache</td><td>{props.version.version}</td></tr>
                 <tr><td>PHP</td><td>{props.version.php}</td></tr>
-                <tr><td>Host</td><td>{props.version.host}</td></tr>
-                <tr><td>Server Software</td><td>{props.version.server}</td></tr>
-                { props.start ? <tr><td>Start time</td><td>{props.start}</td></tr> : null }
-                { props.reset ? <tr><td>Last reset</td><td>{props.reset}</td></tr> : null }
+                <tr><td>{props.txt('Host')}</td><td>{props.version.host}</td></tr>
+                <tr><td>{props.txt('Server Software')}</td><td>{props.version.server}</td></tr>
+                { props.start ? <tr><td>{props.txt('Start time')}</td><td>{props.start}</td></tr> : null }
+                { props.reset ? <tr><td>{props.txt('Last reset')}</td><td>{props.reset}</td></tr> : null }
             </tbody>
         </table>
     );
@@ -352,9 +373,9 @@ function Directives(props) {
         });
         let vShow;
         if (directive.v === true || directive.v === false) {
-            vShow = React.createElement('i', {}, directive.v.toString());
+            vShow = React.createElement('i', {}, props.txt(directive.v.toString()));
         } else if (directive.v === '') {
-            vShow = React.createElement('i', {}, 'no value');
+            vShow = React.createElement('i', {}, props.txt('no value'));
         } else {
             if (Array.isArray(directive.v)) {
                 vShow = directiveList(directive);
@@ -364,7 +385,7 @@ function Directives(props) {
         }
         return (
             <tr key={directive.k}>
-                <td title={'View ' + directive.k + ' manual entry'}><a href={'https://php.net/manual/en/opcache.configuration.php#ini.'
+                <td title={props.txt('View {0} manual entry', directive.k)}><a href={'https://php.net/manual/en/opcache.configuration.php#ini.'
                 + (directive.k).replace(/_/g,'-')} target="_blank">{dShow}</a></td>
                 <td>{vShow}</td>
             </tr>
@@ -373,7 +394,7 @@ function Directives(props) {
 
     return (
         <table className="tables directives-table">
-            <thead><tr><th colSpan="2">Directives</th></tr></thead>
+            <thead><tr><th colSpan="2">{props.txt('Directives')}</th></tr></thead>
             <tbody>{directiveNodes}</tbody>
         </table>
     );
@@ -383,10 +404,10 @@ function Functions(props) {
     return (
         <div id="functions">
             <table className="tables">
-                <thead><tr><th>Available functions</th></tr></thead>
+                <thead><tr><th>{props.txt('Available functions')}</th></tr></thead>
                 <tbody>
                 {props.functions.map(f =>
-                    <tr key={f}><td><a href={"https://php.net/"+f} title="View manual page" target="_blank">{f}</a></td></tr>
+                    <tr key={f}><td><a href={"https://php.net/"+f} title={props.txt('View manual page')} target="_blank">{f}</a></td></tr>
                 )}
                 </tbody>
             </table>
@@ -619,13 +640,13 @@ function MemoryUsagePanel(props) {
         <div className="widget-panel">
             <h3 className="widget-header">memory usage</h3>
             <div className="widget-value widget-info">
-                <p><b>total memory:</b> {props.total}</p>
-                <p><b>used memory:</b> {props.used}</p>
-                <p><b>free memory:</b> {props.free}</p>
-                { props.preload && <p><b>preload memory:</b> {props.preload}</p> }
-                <p><b>wasted memory:</b> {props.wasted} ({props.wastedPercent}%)</p>
-                { props.jitBuffer && <p><b>jit buffer:</b> {props.jitBuffer}</p> }
-                { props.jitBufferFree && <p><b>jit buffer free:</b> {props.jitBufferFree} ({100 - props.jitBufferFreePercentage}%)</p> }
+                <p><b>{props.txt('total memory')}:</b> {props.total}</p>
+                <p><b>{props.txt('used memory')}:</b> {props.used}</p>
+                <p><b>{props.txt('free memory')}:</b> {props.free}</p>
+                { props.preload && <p><b>{props.txt('preload memory')}:</b> {props.preload}</p> }
+                <p><b>{props.txt('wasted memory')}:</b> {props.wasted} ({props.wastedPercent}%)</p>
+                { props.jitBuffer && <p><b>{props.txt('jit buffer')}:</b> {props.jitBuffer}</p> }
+                { props.jitBufferFree && <p><b>{props.txt('jit buffer free')}:</b> {props.jitBufferFree} ({100 - props.jitBufferFreePercentage}%)</p> }
             </div>
         </div>
     );
@@ -635,14 +656,14 @@ function MemoryUsagePanel(props) {
 function StatisticsPanel(props) {
     return (
         <div className="widget-panel">
-            <h3 className="widget-header">opcache statistics</h3>
+            <h3 className="widget-header">{props.txt('opcache statistics')}</h3>
             <div className="widget-value widget-info">
-                <p><b>number of cached files:</b> {props.num_cached_scripts}</p>
-                <p><b>number of hits:</b> {props.hits}</p>
-                <p><b>number of misses:</b> {props.misses}</p>
-                <p><b>blacklist misses:</b> {props.blacklist_miss}</p>
-                <p><b>number of cached keys:</b> {props.num_cached_keys}</p>
-                <p><b>max cached keys:</b> {props.max_cached_keys}</p>
+                <p><b>{props.txt('number of cached')} files:</b> {props.num_cached_scripts}</p>
+                <p><b>{props.txt('number of hits')}:</b> {props.hits}</p>
+                <p><b>{props.txt('number of misses')}:</b> {props.misses}</p>
+                <p><b>{props.txt('blacklist misses')}:</b> {props.blacklist_miss}</p>
+                <p><b>{props.txt('number of cached keys')}:</b> {props.num_cached_keys}</p>
+                <p><b>{props.txt('max cached keys')}:</b> {props.max_cached_keys}</p>
             </div>
         </div>
     );
@@ -652,12 +673,12 @@ function StatisticsPanel(props) {
 function InternedStringsPanel(props) {
     return (
         <div className="widget-panel">
-            <h3 className="widget-header">interned strings usage</h3>
+            <h3 className="widget-header">{props.txt('interned strings usage')}</h3>
             <div className="widget-value widget-info">
-                <p><b>buffer size:</b> {props.buffer_size}</p>
-                <p><b>used memory:</b> {props.strings_used_memory}</p>
-                <p><b>free memory:</b> {props.strings_free_memory}</p>
-                <p><b>number of strings:</b> {props.number_of_strings}</p>
+                <p><b>{props.txt('buffer size')}:</b> {props.buffer_size}</p>
+                <p><b>{props.txt('used memory')}:</b> {props.strings_used_memory}</p>
+                <p><b>{props.txt('free memory')}:</b> {props.strings_free_memory}</p>
+                <p><b>{props.txt('number of strings')}:</b> {props.number_of_strings}</p>
             </div>
         </div>
     );
@@ -732,7 +753,7 @@ class CachedFiles extends React.Component {
         }
 
         if (this.props.allFiles.length === 0) {
-            return <p>No files have been cached or you have <i>opcache.file_cache_only</i> turned on</p>;
+            return <p>{this.props.txt('No files have been cached or you have <i>opcache.file_cache_only</i> turned on')}</p>;
         }
 
         const { searchTerm, currentPage } = this.state;
@@ -752,18 +773,19 @@ class CachedFiles extends React.Component {
         );
         const allFilesTotal = this.props.allFiles.length;
         const showingTotal = filesInSearch.length;
+        const showing = showingTotal !== allFilesTotal ? ", {1} showing due to filter '{2}'" : "";
 
         return (
             <div>
                 <form action="#">
-                    <label htmlFor="frmFilter">Start typing to filter on script path</label><br/>
+                    <label htmlFor="frmFilter">{this.props.txt('Start typing to filter on script path')}</label><br/>
                     <input type="text" name="filter" id="frmFilter" className="file-filter" onChange={e => {this.setSearchTerm(e.target.value)}} />
                 </form>
 
-                <h3>{allFilesTotal} files cached{showingTotal !== allFilesTotal && `, ${showingTotal} showing due to filter '${this.state.searchTerm}'`}</h3>
+                <h3>{this.props.txt(`{0} files cached${showing}`, allFilesTotal, showingTotal, this.state.searchTerm)}</h3>
 
                 { this.props.allow.invalidate && this.state.searchTerm && showingTotal !== allFilesTotal &&
-                    <p><a href={`?invalidate_searched=${encodeURIComponent(this.state.searchTerm)}`} onClick={this.handleInvalidate}>Invalidate all matching files</a></p>
+                    <p><a href={`?invalidate_searched=${encodeURIComponent(this.state.searchTerm)}`} onClick={this.handleInvalidate}>{this.props.txt('Invalidate all matching files')}</a></p>
                 }
 
                 <div className="paginate-filter">
@@ -773,18 +795,19 @@ class CachedFiles extends React.Component {
                         pageNeighbours={2}
                         onPageChanged={this.onPageChanged}
                         refresh={this.state.refreshPagination}
+                        txt={this.props.txt}
                     />}
-                    <nav className="filter" aria-label="Sort order">
+                    <nav className="filter" aria-label={this.props.txt('Sort order')}>
                         <select name="sortBy" onChange={this.changeSort} value={this.state.sortBy}>
-                            <option value="last_used_timestamp">Last used</option>
-                            <option value="last_modified">Last modified</option>
-                            <option value="full_path">Path</option>
-                            <option value="hits">Number of hits</option>
-                            <option value="memory_consumption">Memory consumption</option>
+                            <option value="last_used_timestamp">{this.props.txt('Last used')}</option>
+                            <option value="last_modified">{this.props.txt('Last modified')}</option>
+                            <option value="full_path">{this.props.txt('Path')}</option>
+                            <option value="hits">{this.props.txt('Number of hits')}</option>
+                            <option value="memory_consumption">{this.props.txt('Memory consumption')}</option>
                         </select>
                         <select name="sortDir" onChange={this.changeSort} value={this.state.sortDir}>
-                            <option value="desc">Descending</option>
-                            <option value="asc">Ascending</option>
+                            <option value="desc">{this.props.txt('Descending')}</option>
+                            <option value="asc">{this.props.txt('Ascending')}</option>
                         </select>
                     </nav>
                 </div>
@@ -792,7 +815,7 @@ class CachedFiles extends React.Component {
                 <table className="tables cached-list-table">
                     <thead>
                     <tr>
-                        <th>Script</th>
+                        <th>{this.props.txt('Script')}</th>
                     </tr>
                     </thead>
                     <tbody>
@@ -801,6 +824,7 @@ class CachedFiles extends React.Component {
                             key={file.full_path}
                             canInvalidate={this.props.allow.invalidate}
                             realtime={this.props.realtime}
+                            txt={this.props.txt}
                             {...file}
                         />
                     })}
@@ -831,15 +855,15 @@ class CachedFile extends React.Component {
                 <td>
                     <span className="file-pathname">{this.props.full_path}</span>
                     <span className="file-metainfo">
-                        <b>hits: </b><span>{this.props.readable.hits}, </span>
-                        <b>memory: </b><span>{this.props.readable.memory_consumption}, </span>
-                        { this.props.last_modified && <><b>last modified: </b><span>{this.props.last_modified}, </span></> }
-                        <b>last used: </b><span>{this.props.last_used}</span>
+                        <b>{this.props.txt('hits')}: </b><span>{this.props.readable.hits}, </span>
+                        <b>{this.props.txt('memory')}: </b><span>{this.props.readable.memory_consumption}, </span>
+                        { this.props.last_modified && <><b>{this.props.txt('last modified')}: </b><span>{this.props.last_modified}, </span></> }
+                        <b>{this.props.txt('last used')}: </b><span>{this.props.last_used}</span>
                     </span>
-                    { !this.props.timestamp && <span className="invalid file-metainfo"> - has been invalidated</span> }
+                    { !this.props.timestamp && <span className="invalid file-metainfo"> - {this.props.txt('has been invalidated')}</span> }
                     { this.props.canInvalidate && <span>,&nbsp;<a className="file-metainfo"
                           href={'?invalidate=' + this.props.full_path} data-file={this.props.full_path}
-                          onClick={this.handleInvalidate}>force file invalidation</a></span> }
+                          onClick={this.handleInvalidate}>{this.props.txt('force file invalidation')}</a></span> }
                 </td>
             </tr>
         );
@@ -869,7 +893,7 @@ class IgnoredFiles extends React.Component {
         }
 
         if (this.props.allFiles.length === 0) {
-            return <p>No files have been ignored via <i>opcache.blacklist_filename</i></p>;
+            return <p>{this.props.txt('No files have been ignored via <i>opcache.blacklist_filename</i>')}</p>;
         }
 
         const { currentPage } = this.state;
@@ -882,7 +906,7 @@ class IgnoredFiles extends React.Component {
 
         return (
             <div>
-                <h3>{allFilesTotal} ignore file locations</h3>
+                <h3>{this.props.txt('{0} ignore file locations', allFilesTotal)}</h3>
 
                 {this.doPagination && <Pagination
                     totalRecords={allFilesTotal}
@@ -890,10 +914,11 @@ class IgnoredFiles extends React.Component {
                     pageNeighbours={2}
                     onPageChanged={this.onPageChanged}
                     refresh={this.state.refreshPagination}
+                    txt={this.props.txt}
                 />}
 
                 <table className="tables ignored-list-table">
-                    <thead><tr><th>Path</th></tr></thead>
+                    <thead><tr><th>{this.props.txt('Path')}</th></tr></thead>
                     <tbody>
                         {filesInPage.map((file, index) => {
                             return <tr key={file}><td>{file}</td></tr>
@@ -928,7 +953,7 @@ class PreloadedFiles extends React.Component {
         }
 
         if (this.props.allFiles.length === 0) {
-            return <p>No files have been preloaded <i>opcache.preload</i></p>;
+            return <p>{this.props.txt('No files have been preloaded <i>opcache.preload</i>')}</p>;
         }
 
         const { currentPage } = this.state;
@@ -941,7 +966,7 @@ class PreloadedFiles extends React.Component {
 
         return (
             <div>
-                <h3>{allFilesTotal} preloaded files</h3>
+                <h3>{this.props.txt('{0} preloaded files', allFilesTotal)}</h3>
 
                 {this.doPagination && <Pagination
                     totalRecords={allFilesTotal}
@@ -949,10 +974,11 @@ class PreloadedFiles extends React.Component {
                     pageNeighbours={2}
                     onPageChanged={this.onPageChanged}
                     refresh={this.state.refreshPagination}
+                    txt={this.props.txt}
                 />}
 
                 <table className="tables preload-list-table">
-                    <thead><tr><th>Path</th></tr></thead>
+                    <thead><tr><th>{this.props.txt('Path')}</th></tr></thead>
                     <tbody>
                         {filesInPage.map((file, index) => {
                             return <tr key={file}><td>{file}</td></tr>
@@ -1086,15 +1112,15 @@ class Pagination extends React.Component {
                             return (
                                 <React.Fragment key={index}>
                                     <li className="page-item arrow">
-                                        <a className="page-link" href="#" aria-label="Previous" onClick={this.handleJumpLeft}>
+                                        <a className="page-link" href="#" aria-label={this.props.txt('Previous')} onClick={this.handleJumpLeft}>
                                             <span aria-hidden="true">↞</span>
-                                            <span className="sr-only">Jump back</span>
+                                            <span className="sr-only">{this.props.txt('Jump back')}</span>
                                         </a>
                                     </li>
                                     <li className="page-item arrow">
-                                        <a className="page-link" href="#" aria-label="Previous" onClick={this.handleMoveLeft}>
+                                        <a className="page-link" href="#" aria-label={this.props.txt('Previous')} onClick={this.handleMoveLeft}>
                                             <span aria-hidden="true">⇠</span>
-                                            <span className="sr-only">Previous page</span>
+                                            <span className="sr-only">{this.props.txt('Previous page')}</span>
                                         </a>
                                     </li>
                                 </React.Fragment>
@@ -1104,15 +1130,15 @@ class Pagination extends React.Component {
                             return (
                                 <React.Fragment key={index}>
                                     <li className="page-item arrow">
-                                        <a className="page-link" href="#" aria-label="Next" onClick={this.handleMoveRight}>
+                                        <a className="page-link" href="#" aria-label={this.props.txt('Next')} onClick={this.handleMoveRight}>
                                             <span aria-hidden="true">⇢</span>
-                                            <span className="sr-only">Next page</span>
+                                            <span className="sr-only">{this.props.txt('Next page')}</span>
                                         </a>
                                     </li>
                                     <li className="page-item arrow">
-                                        <a className="page-link" href="#" aria-label="Next" onClick={this.handleJumpRight}>
+                                        <a className="page-link" href="#" aria-label={this.props.txt('Next')} onClick={this.handleJumpRight}>
                                             <span aria-hidden="true">↠</span>
-                                            <span className="sr-only">Jump forward</span>
+                                            <span className="sr-only">{this.props.txt('Jump forward')}</span>
                                         </a>
                                     </li>
                                 </React.Fragment>

+ 2 - 2
build/build.php

@@ -29,8 +29,8 @@ $jsOutput = trim(file_get_contents(__DIR__ . '/interface.js'));
 $cssOutput = trim(file_get_contents(__DIR__ . '/interface.css'));
 $phpOutput = trim(implode('', array_slice(file($parentPath . '/src/Opcache/Service.php'), 3)));
 $output = str_replace(
-    ['{{JS_OUTPUT}}', '{{CSS_OUTPUT}}', '{{PHP_OUTPUT}}'],
-    [$jsOutput, $cssOutput, $phpOutput],
+    ['{{JS_OUTPUT}}', '{{CSS_OUTPUT}}', '{{PHP_OUTPUT}}', '{{LANGUAGE_PACK}}'],
+    [$jsOutput, $cssOutput, $phpOutput, 'null'],
     $template
 );
 if ($makeJsLocal) {

+ 4 - 2
build/template.phps

@@ -39,7 +39,8 @@ $options = [
         'keys'   => true,                      // show the keys used chart/big number
         'jit'    => true                       // show the jit buffer chart/big number
     ],
-    'language_pack'    => null                 // json structure of all text strings used, or null for default
+    // json structure of all text strings used, or null for default
+    'language_pack'    => {{LANGUAGE_PACK}}
 ];
 
 /*
@@ -101,7 +102,8 @@ $opcache = (new Service($options))->handle();
         highlight: <?= json_encode($opcache->getOption('highlight')); ?>,
         debounceRate: <?= $opcache->getOption('debounce_rate'); ?>,
         perPageLimit: <?= json_encode($opcache->getOption('per_page')); ?>,
-        realtimeRefresh: <?= json_encode($opcache->getOption('refresh_time')); ?>
+        realtimeRefresh: <?= json_encode($opcache->getOption('refresh_time')); ?>,
+        language: <?= json_encode($opcache->getOption('language_pack')); ?>,
     }), document.getElementById('interface'));
 
     </script>

+ 202 - 148
index.php

@@ -20,24 +20,27 @@ namespace Amnuts\Opcache;
  */
 
 $options = [
-    'allow_filelist'   => true,          // show/hide the files tab
-    'allow_invalidate' => true,          // give a link to invalidate files
-    'allow_reset'      => true,          // give option to reset the whole cache
-    'allow_realtime'   => true,          // give option to enable/disable real-time updates
-    'refresh_time'     => 5,             // how often the data will refresh, in seconds
-    'size_precision'   => 2,             // Digits after decimal point
-    'size_space'       => false,         // have '1MB' or '1 MB' when showing sizes
-    'charts'           => true,          // show gauge chart or just big numbers
-    'debounce_rate'    => 250,           // milliseconds after key press to send keyup event when filtering
-    'per_page'         => 200,           // How many results per page to show in the file list, false for no pagination
-    'cookie_name'      => 'opcachegui',  // name of cookie
-    'cookie_ttl'       => 365,           // days to store cookie
+    'allow_filelist'   => true,                // show/hide the files tab
+    'allow_invalidate' => true,                // give a link to invalidate files
+    'allow_reset'      => true,                // give option to reset the whole cache
+    'allow_realtime'   => true,                // give option to enable/disable real-time updates
+    'refresh_time'     => 5,                   // how often the data will refresh, in seconds
+    'size_precision'   => 2,                   // Digits after decimal point
+    'size_space'       => false,               // have '1MB' or '1 MB' when showing sizes
+    'charts'           => true,                // show gauge chart or just big numbers
+    'debounce_rate'    => 250,                 // milliseconds after key press to send keyup event when filtering
+    'per_page'         => 200,                 // How many results per page to show in the file list, false for no pagination
+    'cookie_name'      => 'opcachegui',        // name of cookie
+    'cookie_ttl'       => 365,                 // days to store cookie
+    'datetime_format'  => 'D, d M Y H:i:s O',  // Show datetime in this format
     'highlight'        => [
-        'memory' => true,                // show the memory chart/big number
-        'hits'   => true,                // show the hit rate chart/big number
-        'keys'   => true,                // show the keys used chart/big number
-        'jit'    => true                 // show the jit buffer chart/big number
-    ]
+        'memory' => true,                      // show the memory chart/big number
+        'hits'   => true,                      // show the hit rate chart/big number
+        'keys'   => true,                      // show the keys used chart/big number
+        'jit'    => true                       // show the jit buffer chart/big number
+    ],
+    // json structure of all text strings used, or null for default
+    'language_pack'    => null
 ];
 
 /*
@@ -68,6 +71,12 @@ class Service
     protected $data;
     protected $options;
     protected $optimizationLevels;
+    protected $jitModes;
+    protected $jitModeMapping = [
+        'tracing' => 1254,
+        'on' => 1254,
+        'function' => 1205
+    ];
     protected $defaults = [
         'allow_filelist'   => true,                // show/hide the files tab
         'allow_invalidate' => true,                // give a link to invalidate files
@@ -87,51 +96,8 @@ class Service
             'hits'   => true,                      // show the hit rate chart/big number
             'keys'   => true,                      // show the keys used chart/big number
             'jit'    => true                       // show the jit buffer chart/big number
-        ]
-    ];
-    protected $jitModes = [
-        [
-            'flag' => 'CPU-specific optimization',
-            'value' => [
-                'Disable CPU-specific optimization',
-                'Enable use of AVX, if the CPU supports it'
-            ]
         ],
-        [
-            'flag' => 'Register allocation',
-            'value' => [
-                'Do not perform register allocation',
-                'Perform block-local register allocation',
-                'Perform global register allocation'
-            ]
-        ],
-        [
-            'flag' => 'Trigger',
-            'value' => [
-                'Compile all functions on script load',
-                'Compile functions on first execution',
-                'Profile functions on first request and compile the hottest functions afterwards',
-                'Profile on the fly and compile hot functions',
-                'Currently unused',
-                'Use tracing JIT. Profile on the fly and compile traces for hot code segments'
-            ]
-        ],
-        [
-            'flag' => 'Optimization level',
-            'value' => [
-                'No JIT',
-                'Minimal JIT (call standard VM handlers)',
-                'Inline VM handlers',
-                'Use type inference',
-                'Use call graph',
-                'Optimize whole script'
-            ]
-        ]
-    ];
-    protected $jitModeMapping = [
-        'tracing' => 1254,
-        'on' => 1254,
-        'function' => 1205
+        'language_pack'    => null                 // json structure of all text strings used, or null for default
     ];
 
     /**
@@ -141,29 +107,89 @@ class Service
      */
     public function __construct(array $options = [])
     {
-        $this->optimizationLevels = [
-            1 << 0 => 'CSE, STRING construction',
-            1 << 1 => 'Constant conversion and jumps',
-            1 << 2 => '++, +=, series of jumps',
-            1 << 3 => 'INIT_FCALL_BY_NAME -> DO_FCALL',
-            1 << 4 => 'CFG based optimization',
-            1 << 5 => 'DFA based optimization',
-            1 << 6 => 'CALL GRAPH optimization',
-            1 << 7 => 'SCCP (constant propagation)',
-            1 << 8 => 'TMP VAR usage',
-            1 << 9 => 'NOP removal',
-            1 << 10 => 'Merge equal constants',
-            1 << 11 => 'Adjust used stack',
-            1 << 12 => 'Remove unused variables',
-            1 << 13 => 'DCE (dead code elimination)',
-            1 << 14 => '(unsafe) Collect constants',
-            1 << 15 => 'Inline functions'
-        ];
         $this->options = array_merge($this->defaults, $options);
         $this->tz = new DateTimeZone(date_default_timezone_get());
+        if (is_string($this->options['language_pack'])) {
+            $this->options['language_pack'] = json_decode($this->options['language_pack'], true);
+        }
+
+        $this->optimizationLevels = [
+            1 << 0  => $this->txt('CSE, STRING construction'),
+            1 << 1  => $this->txt('Constant conversion and jumps'),
+            1 << 2  => $this->txt('++, +=, series of jumps'),
+            1 << 3  => $this->txt('INIT_FCALL_BY_NAME -> DO_FCALL'),
+            1 << 4  => $this->txt('CFG based optimization'),
+            1 << 5  => $this->txt('DFA based optimization'),
+            1 << 6  => $this->txt('CALL GRAPH optimization'),
+            1 << 7  => $this->txt('SCCP (constant propagation)'),
+            1 << 8  => $this->txt('TMP VAR usage'),
+            1 << 9  => $this->txt('NOP removal'),
+            1 << 10 => $this->txt('Merge equal constants'),
+            1 << 11 => $this->txt('Adjust used stack'),
+            1 << 12 => $this->txt('Remove unused variables'),
+            1 << 13 => $this->txt('DCE (dead code elimination)'),
+            1 << 14 => $this->txt('(unsafe) Collect constants'),
+            1 << 15 => $this->txt('Inline functions'),
+        ];
+        $this->jitModes = [
+            [
+                'flag' => $this->txt('CPU-specific optimization'),
+                'value' => [
+                    $this->txt('Disable CPU-specific optimization'),
+                    $this->txt('Enable use of AVX, if the CPU supports it')
+                ]
+            ],
+            [
+                'flag' => $this->txt('Register allocation'),
+                'value' => [
+                    $this->txt('Do not perform register allocation'),
+                    $this->txt('Perform block-local register allocation'),
+                    $this->txt('Perform global register allocation')
+                ]
+            ],
+            [
+                'flag' => $this->txt('Trigger'),
+                'value' => [
+                    $this->txt('Compile all functions on script load'),
+                    $this->txt('Compile functions on first execution'),
+                    $this->txt('Profile functions on first request and compile the hottest functions afterwards'),
+                    $this->txt('Profile on the fly and compile hot functions'),
+                    $this->txt('Currently unused'),
+                    $this->txt('Use tracing JIT. Profile on the fly and compile traces for hot code segments')
+                ]
+            ],
+            [
+                'flag' => $this->txt('Optimization level'),
+                'value' => [
+                    $this->txt('No JIT'),
+                    $this->txt('Minimal JIT (call standard VM handlers)'),
+                    $this->txt('Inline VM handlers'),
+                    $this->txt('Use type inference'),
+                    $this->txt('Use call graph'),
+                    $this->txt('Optimize whole script')
+                ]
+            ]
+        ];
+
         $this->data = $this->compileState();
     }
 
+    /**
+     * @return string
+     */
+    public function txt(): string
+    {
+        $args = func_get_args();
+        $text = array_shift($args);
+        if ((($lang = $this->getOption('language_pack')) !== null) && !empty($lang[$text])) {
+            $text = $lang[$text];
+        }
+        foreach ($args as $i => $arg) {
+            $text = str_replace('{' . $i . '}', $arg, $text);
+        }
+        return $text;
+    }
+
     /**
      * @return $this
      * @throws Exception
@@ -575,6 +601,17 @@ class Interface extends React.Component {
       return v ? !!v[2] : false;
     });
 
+    _defineProperty(this, "txt", (text, ...args) => {
+      if (this.props.language !== null && this.props.language.hasOwnProperty(text) && this.props.language[text]) {
+        text = this.props.language[text];
+      }
+
+      args.forEach((arg, i) => {
+        text = text.replaceAll(`{${i}}`, arg);
+      });
+      return text;
+    });
+
     this.state = {
       realtime: this.getCookie(),
       resetting: false,
@@ -599,7 +636,8 @@ class Interface extends React.Component {
       realtime: this.state.realtime,
       resetting: this.state.resetting,
       realtimeHandler: this.realtimeHandler,
-      resetHandler: this.resetHandler
+      resetHandler: this.resetHandler,
+      txt: this.txt
     }))), /*#__PURE__*/React.createElement(Footer, {
       version: this.props.opstate.version.gui
     }));
@@ -611,26 +649,30 @@ function MainNavigation(props) {
   return /*#__PURE__*/React.createElement("nav", {
     className: "main-nav"
   }, /*#__PURE__*/React.createElement(Tabs, null, /*#__PURE__*/React.createElement("div", {
-    label: "Overview",
+    label: props.txt("Overview"),
     tabId: "overview",
     tabIndex: 1
   }, /*#__PURE__*/React.createElement(OverviewCounts, {
     overview: props.opstate.overview,
     highlight: props.highlight,
-    useCharts: props.useCharts
+    useCharts: props.useCharts,
+    txt: props.txt
   }), /*#__PURE__*/React.createElement("div", {
     id: "info",
     className: "tab-content-overview-info"
   }, /*#__PURE__*/React.createElement(GeneralInfo, {
     start: props.opstate.overview && props.opstate.overview.readable.start_time || null,
     reset: props.opstate.overview && props.opstate.overview.readable.last_restart_time || null,
-    version: props.opstate.version
+    version: props.opstate.version,
+    txt: props.txt
   }), /*#__PURE__*/React.createElement(Directives, {
-    directives: props.opstate.directives
+    directives: props.opstate.directives,
+    txt: props.txt
   }), /*#__PURE__*/React.createElement(Functions, {
-    functions: props.opstate.functions
+    functions: props.opstate.functions,
+    txt: props.txt
   }))), props.allow.filelist && /*#__PURE__*/React.createElement("div", {
-    label: "Cached",
+    label: props.txt("Cached"),
     tabId: "cached",
     tabIndex: 2
   }, /*#__PURE__*/React.createElement(CachedFiles, {
@@ -642,9 +684,10 @@ function MainNavigation(props) {
       fileList: props.allow.filelist,
       invalidate: props.allow.invalidate
     },
-    realtime: props.realtime
+    realtime: props.realtime,
+    txt: props.txt
   })), props.allow.filelist && props.opstate.blacklist.length && /*#__PURE__*/React.createElement("div", {
-    label: "Ignored",
+    label: props.txt("Ignored"),
     tabId: "ignored",
     tabIndex: 3
   }, /*#__PURE__*/React.createElement(IgnoredFiles, {
@@ -652,9 +695,10 @@ function MainNavigation(props) {
     allFiles: props.opstate.blacklist,
     allow: {
       fileList: props.allow.filelist
-    }
+    },
+    txt: props.txt
   })), props.allow.filelist && props.opstate.preload.length && /*#__PURE__*/React.createElement("div", {
-    label: "Preloaded",
+    label: props.txt("Preloaded"),
     tabId: "preloaded",
     tabIndex: 4
   }, /*#__PURE__*/React.createElement(PreloadedFiles, {
@@ -662,15 +706,16 @@ function MainNavigation(props) {
     allFiles: props.opstate.preload,
     allow: {
       fileList: props.allow.filelist
-    }
+    },
+    txt: props.txt
   })), props.allow.reset && /*#__PURE__*/React.createElement("div", {
-    label: "Reset cache",
+    label: props.txt("Reset cache"),
     tabId: "resetCache",
     className: `nav-tab-link-reset${props.resetting ? ' is-resetting pulse' : ''}`,
     handler: props.resetHandler,
     tabIndex: 5
   }), props.allow.realtime && /*#__PURE__*/React.createElement("div", {
-    label: `${props.realtime ? 'Disable' : 'Enable'} real-time update`,
+    label: props.txt(`${props.realtime ? 'Disable' : 'Enable'} real-time update`),
     tabId: "toggleRealtime",
     className: `nav-tab-link-realtime${props.realtime ? ' live-update pulse' : ''}`,
     handler: props.realtimeHandler,
@@ -781,27 +826,27 @@ function OverviewCounts(props) {
   if (props.overview === false) {
     return /*#__PURE__*/React.createElement("p", {
       class: "file-cache-only"
-    }, "You have ", /*#__PURE__*/React.createElement("i", null, "opcache.file_cache_only"), " turned on.  As a result, the memory information is not available.  Statistics and file list may also not be returned by ", /*#__PURE__*/React.createElement("i", null, "opcache_get_statistics()"), ".");
+    }, props.txt(`You have <i>opcache.file_cache_only</i> turned on.  As a result, the memory information is not available.  Statistics and file list may also not be returned by <i>opcache_get_statistics()</i>.`));
   }
 
   const graphList = [{
     id: 'memoryUsageCanvas',
-    title: 'memory',
+    title: props.txt('memory'),
     show: props.highlight.memory,
     value: props.overview.used_memory_percentage
   }, {
     id: 'hitRateCanvas',
-    title: 'hit rate',
+    title: props.txt('hit rate'),
     show: props.highlight.hits,
     value: props.overview.hit_rate_percentage
   }, {
     id: 'keyUsageCanvas',
-    title: 'keys',
+    title: props.txt('keys'),
     show: props.highlight.keys,
     value: props.overview.used_key_percentage
   }, {
     id: 'jitUsageCanvas',
-    title: 'jit buffer',
+    title: props.txt('jit buffer'),
     show: props.highlight.jit,
     value: props.overview.jit_buffer_used_percentage
   }];
@@ -832,19 +877,22 @@ function OverviewCounts(props) {
     wastedPercent: props.overview.wasted_percentage,
     jitBuffer: props.overview.readable.jit_buffer_size || null,
     jitBufferFree: props.overview.readable.jit_buffer_free || null,
-    jitBufferFreePercentage: props.overview.jit_buffer_used_percentage || null
+    jitBufferFreePercentage: props.overview.jit_buffer_used_percentage || null,
+    txt: props.txt
   }), /*#__PURE__*/React.createElement(StatisticsPanel, {
     num_cached_scripts: props.overview.readable.num_cached_scripts,
     hits: props.overview.readable.hits,
     misses: props.overview.readable.misses,
     blacklist_miss: props.overview.readable.blacklist_miss,
     num_cached_keys: props.overview.readable.num_cached_keys,
-    max_cached_keys: props.overview.readable.max_cached_keys
+    max_cached_keys: props.overview.readable.max_cached_keys,
+    txt: props.txt
   }), props.overview.readable.interned && /*#__PURE__*/React.createElement(InternedStringsPanel, {
     buffer_size: props.overview.readable.interned.buffer_size,
     strings_used_memory: props.overview.readable.interned.strings_used_memory,
     strings_free_memory: props.overview.readable.interned.strings_free_memory,
-    number_of_strings: props.overview.readable.interned.number_of_strings
+    number_of_strings: props.overview.readable.interned.number_of_strings,
+    txt: props.txt
   }));
 }
 
@@ -853,7 +901,7 @@ function GeneralInfo(props) {
     className: "tables general-info-table"
   }, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", {
     colSpan: "2"
-  }, "General info"))), /*#__PURE__*/React.createElement("tbody", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("td", null, "Zend OPcache"), /*#__PURE__*/React.createElement("td", null, props.version.version)), /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("td", null, "PHP"), /*#__PURE__*/React.createElement("td", null, props.version.php)), /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("td", null, "Host"), /*#__PURE__*/React.createElement("td", null, props.version.host)), /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("td", null, "Server Software"), /*#__PURE__*/React.createElement("td", null, props.version.server)), props.start ? /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("td", null, "Start time"), /*#__PURE__*/React.createElement("td", null, props.start)) : null, props.reset ? /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("td", null, "Last reset"), /*#__PURE__*/React.createElement("td", null, props.reset)) : null));
+  }, props.txt('General info')))), /*#__PURE__*/React.createElement("tbody", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("td", null, "Zend OPcache"), /*#__PURE__*/React.createElement("td", null, props.version.version)), /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("td", null, "PHP"), /*#__PURE__*/React.createElement("td", null, props.version.php)), /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("td", null, props.txt('Host')), /*#__PURE__*/React.createElement("td", null, props.version.host)), /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("td", null, props.txt('Server Software')), /*#__PURE__*/React.createElement("td", null, props.version.server)), props.start ? /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("td", null, props.txt('Start time')), /*#__PURE__*/React.createElement("td", null, props.start)) : null, props.reset ? /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("td", null, props.txt('Last reset')), /*#__PURE__*/React.createElement("td", null, props.reset)) : null));
 }
 
 function Directives(props) {
@@ -882,9 +930,9 @@ function Directives(props) {
     let vShow;
 
     if (directive.v === true || directive.v === false) {
-      vShow = React.createElement('i', {}, directive.v.toString());
+      vShow = React.createElement('i', {}, props.txt(directive.v.toString()));
     } else if (directive.v === '') {
-      vShow = React.createElement('i', {}, 'no value');
+      vShow = React.createElement('i', {}, props.txt('no value'));
     } else {
       if (Array.isArray(directive.v)) {
         vShow = directiveList(directive);
@@ -896,7 +944,7 @@ function Directives(props) {
     return /*#__PURE__*/React.createElement("tr", {
       key: directive.k
     }, /*#__PURE__*/React.createElement("td", {
-      title: 'View ' + directive.k + ' manual entry'
+      title: props.txt('View {0} manual entry', directive.k)
     }, /*#__PURE__*/React.createElement("a", {
       href: 'https://php.net/manual/en/opcache.configuration.php#ini.' + directive.k.replace(/_/g, '-'),
       target: "_blank"
@@ -906,7 +954,7 @@ function Directives(props) {
     className: "tables directives-table"
   }, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", {
     colSpan: "2"
-  }, "Directives"))), /*#__PURE__*/React.createElement("tbody", null, directiveNodes));
+  }, props.txt('Directives')))), /*#__PURE__*/React.createElement("tbody", null, directiveNodes));
 }
 
 function Functions(props) {
@@ -914,11 +962,11 @@ function Functions(props) {
     id: "functions"
   }, /*#__PURE__*/React.createElement("table", {
     className: "tables"
-  }, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", null, "Available functions"))), /*#__PURE__*/React.createElement("tbody", null, props.functions.map(f => /*#__PURE__*/React.createElement("tr", {
+  }, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", null, props.txt('Available functions')))), /*#__PURE__*/React.createElement("tbody", null, props.functions.map(f => /*#__PURE__*/React.createElement("tr", {
     key: f
   }, /*#__PURE__*/React.createElement("td", null, /*#__PURE__*/React.createElement("a", {
     href: "https://php.net/" + f,
-    title: "View manual page",
+    title: props.txt('View manual page'),
     target: "_blank"
   }, f)))))));
 }
@@ -1149,7 +1197,7 @@ function MemoryUsagePanel(props) {
     className: "widget-header"
   }, "memory usage"), /*#__PURE__*/React.createElement("div", {
     className: "widget-value widget-info"
-  }, /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, "total memory:"), " ", props.total), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, "used memory:"), " ", props.used), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, "free memory:"), " ", props.free), props.preload && /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, "preload memory:"), " ", props.preload), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, "wasted memory:"), " ", props.wasted, " (", props.wastedPercent, "%)"), props.jitBuffer && /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, "jit buffer:"), " ", props.jitBuffer), props.jitBufferFree && /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, "jit buffer free:"), " ", props.jitBufferFree, " (", 100 - props.jitBufferFreePercentage, "%)")));
+  }, /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, props.txt('total memory'), ":"), " ", props.total), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, props.txt('used memory'), ":"), " ", props.used), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, props.txt('free memory'), ":"), " ", props.free), props.preload && /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, props.txt('preload memory'), ":"), " ", props.preload), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, props.txt('wasted memory'), ":"), " ", props.wasted, " (", props.wastedPercent, "%)"), props.jitBuffer && /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, props.txt('jit buffer'), ":"), " ", props.jitBuffer), props.jitBufferFree && /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, props.txt('jit buffer free'), ":"), " ", props.jitBufferFree, " (", 100 - props.jitBufferFreePercentage, "%)")));
 }
 
 function StatisticsPanel(props) {
@@ -1157,9 +1205,9 @@ function StatisticsPanel(props) {
     className: "widget-panel"
   }, /*#__PURE__*/React.createElement("h3", {
     className: "widget-header"
-  }, "opcache statistics"), /*#__PURE__*/React.createElement("div", {
+  }, props.txt('opcache statistics')), /*#__PURE__*/React.createElement("div", {
     className: "widget-value widget-info"
-  }, /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, "number of cached files:"), " ", props.num_cached_scripts), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, "number of hits:"), " ", props.hits), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, "number of misses:"), " ", props.misses), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, "blacklist misses:"), " ", props.blacklist_miss), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, "number of cached keys:"), " ", props.num_cached_keys), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, "max cached keys:"), " ", props.max_cached_keys)));
+  }, /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, props.txt('number of cached'), " files:"), " ", props.num_cached_scripts), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, props.txt('number of hits'), ":"), " ", props.hits), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, props.txt('number of misses'), ":"), " ", props.misses), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, props.txt('blacklist misses'), ":"), " ", props.blacklist_miss), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, props.txt('number of cached keys'), ":"), " ", props.num_cached_keys), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, props.txt('max cached keys'), ":"), " ", props.max_cached_keys)));
 }
 
 function InternedStringsPanel(props) {
@@ -1167,9 +1215,9 @@ function InternedStringsPanel(props) {
     className: "widget-panel"
   }, /*#__PURE__*/React.createElement("h3", {
     className: "widget-header"
-  }, "interned strings usage"), /*#__PURE__*/React.createElement("div", {
+  }, props.txt('interned strings usage')), /*#__PURE__*/React.createElement("div", {
     className: "widget-value widget-info"
-  }, /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, "buffer size:"), " ", props.buffer_size), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, "used memory:"), " ", props.strings_used_memory), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, "free memory:"), " ", props.strings_free_memory), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, "number of strings:"), " ", props.number_of_strings)));
+  }, /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, props.txt('buffer size'), ":"), " ", props.buffer_size), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, props.txt('used memory'), ":"), " ", props.strings_used_memory), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, props.txt('free memory'), ":"), " ", props.strings_free_memory), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("b", null, props.txt('number of strings'), ":"), " ", props.number_of_strings)));
 }
 
 class CachedFiles extends React.Component {
@@ -1247,7 +1295,7 @@ class CachedFiles extends React.Component {
     }
 
     if (this.props.allFiles.length === 0) {
-      return /*#__PURE__*/React.createElement("p", null, "No files have been cached or you have ", /*#__PURE__*/React.createElement("i", null, "opcache.file_cache_only"), " turned on");
+      return /*#__PURE__*/React.createElement("p", null, this.props.txt('No files have been cached or you have <i>opcache.file_cache_only</i> turned on'));
     }
 
     const {
@@ -1262,11 +1310,12 @@ class CachedFiles extends React.Component {
     const filesInPage = this.doPagination ? filesInSearch.slice(offset, offset + this.props.perPageLimit) : filesInSearch;
     const allFilesTotal = this.props.allFiles.length;
     const showingTotal = filesInSearch.length;
+    const showing = showingTotal !== allFilesTotal ? ", {1} showing due to filter '{2}'" : "";
     return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("form", {
       action: "#"
     }, /*#__PURE__*/React.createElement("label", {
       htmlFor: "frmFilter"
-    }, "Start typing to filter on script path"), /*#__PURE__*/React.createElement("br", null), /*#__PURE__*/React.createElement("input", {
+    }, this.props.txt('Start typing to filter on script path')), /*#__PURE__*/React.createElement("br", null), /*#__PURE__*/React.createElement("input", {
       type: "text",
       name: "filter",
       id: "frmFilter",
@@ -1274,49 +1323,51 @@ class CachedFiles extends React.Component {
       onChange: e => {
         this.setSearchTerm(e.target.value);
       }
-    })), /*#__PURE__*/React.createElement("h3", null, allFilesTotal, " files cached", showingTotal !== allFilesTotal && `, ${showingTotal} showing due to filter '${this.state.searchTerm}'`), this.props.allow.invalidate && this.state.searchTerm && showingTotal !== allFilesTotal && /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("a", {
+    })), /*#__PURE__*/React.createElement("h3", null, this.props.txt(`{0} files cached${showing}`, allFilesTotal, showingTotal, this.state.searchTerm)), this.props.allow.invalidate && this.state.searchTerm && showingTotal !== allFilesTotal && /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement("a", {
       href: `?invalidate_searched=${encodeURIComponent(this.state.searchTerm)}`,
       onClick: this.handleInvalidate
-    }, "Invalidate all matching files")), /*#__PURE__*/React.createElement("div", {
+    }, this.props.txt('Invalidate all matching files'))), /*#__PURE__*/React.createElement("div", {
       className: "paginate-filter"
     }, this.doPagination && /*#__PURE__*/React.createElement(Pagination, {
       totalRecords: filesInSearch.length,
       pageLimit: this.props.perPageLimit,
       pageNeighbours: 2,
       onPageChanged: this.onPageChanged,
-      refresh: this.state.refreshPagination
+      refresh: this.state.refreshPagination,
+      txt: this.props.txt
     }), /*#__PURE__*/React.createElement("nav", {
       className: "filter",
-      "aria-label": "Sort order"
+      "aria-label": this.props.txt('Sort order')
     }, /*#__PURE__*/React.createElement("select", {
       name: "sortBy",
       onChange: this.changeSort,
       value: this.state.sortBy
     }, /*#__PURE__*/React.createElement("option", {
       value: "last_used_timestamp"
-    }, "Last used"), /*#__PURE__*/React.createElement("option", {
+    }, this.props.txt('Last used')), /*#__PURE__*/React.createElement("option", {
       value: "last_modified"
-    }, "Last modified"), /*#__PURE__*/React.createElement("option", {
+    }, this.props.txt('Last modified')), /*#__PURE__*/React.createElement("option", {
       value: "full_path"
-    }, "Path"), /*#__PURE__*/React.createElement("option", {
+    }, this.props.txt('Path')), /*#__PURE__*/React.createElement("option", {
       value: "hits"
-    }, "Number of hits"), /*#__PURE__*/React.createElement("option", {
+    }, this.props.txt('Number of hits')), /*#__PURE__*/React.createElement("option", {
       value: "memory_consumption"
-    }, "Memory consumption")), /*#__PURE__*/React.createElement("select", {
+    }, this.props.txt('Memory consumption'))), /*#__PURE__*/React.createElement("select", {
       name: "sortDir",
       onChange: this.changeSort,
       value: this.state.sortDir
     }, /*#__PURE__*/React.createElement("option", {
       value: "desc"
-    }, "Descending"), /*#__PURE__*/React.createElement("option", {
+    }, this.props.txt('Descending')), /*#__PURE__*/React.createElement("option", {
       value: "asc"
-    }, "Ascending")))), /*#__PURE__*/React.createElement("table", {
+    }, this.props.txt('Ascending'))))), /*#__PURE__*/React.createElement("table", {
       className: "tables cached-list-table"
-    }, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", null, "Script"))), /*#__PURE__*/React.createElement("tbody", null, filesInPage.map((file, index) => {
+    }, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", null, this.props.txt('Script')))), /*#__PURE__*/React.createElement("tbody", null, filesInPage.map((file, index) => {
       return /*#__PURE__*/React.createElement(CachedFile, _extends({
         key: file.full_path,
         canInvalidate: this.props.allow.invalidate,
-        realtime: this.props.realtime
+        realtime: this.props.realtime,
+        txt: this.props.txt
       }, file));
     }))));
   }
@@ -1351,14 +1402,14 @@ class CachedFile extends React.Component {
       className: "file-pathname"
     }, this.props.full_path), /*#__PURE__*/React.createElement("span", {
       className: "file-metainfo"
-    }, /*#__PURE__*/React.createElement("b", null, "hits: "), /*#__PURE__*/React.createElement("span", null, this.props.readable.hits, ", "), /*#__PURE__*/React.createElement("b", null, "memory: "), /*#__PURE__*/React.createElement("span", null, this.props.readable.memory_consumption, ", "), this.props.last_modified && /*#__PURE__*/React.createElement("span", null, /*#__PURE__*/React.createElement("b", null, "last modified: "), /*#__PURE__*/React.createElement("span", null, this.props.last_modified, ", ")), /*#__PURE__*/React.createElement("b", null, "last used: "), /*#__PURE__*/React.createElement("span", null, this.props.last_used)), !this.props.timestamp && /*#__PURE__*/React.createElement("span", {
+    }, /*#__PURE__*/React.createElement("b", null, this.props.txt('hits'), ": "), /*#__PURE__*/React.createElement("span", null, this.props.readable.hits, ", "), /*#__PURE__*/React.createElement("b", null, this.props.txt('memory'), ": "), /*#__PURE__*/React.createElement("span", null, this.props.readable.memory_consumption, ", "), this.props.last_modified && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("b", null, this.props.txt('last modified'), ": "), /*#__PURE__*/React.createElement("span", null, this.props.last_modified, ", ")), /*#__PURE__*/React.createElement("b", null, this.props.txt('last used'), ": "), /*#__PURE__*/React.createElement("span", null, this.props.last_used)), !this.props.timestamp && /*#__PURE__*/React.createElement("span", {
       className: "invalid file-metainfo"
-    }, " - has been invalidated"), this.props.canInvalidate && /*#__PURE__*/React.createElement("span", null, ",\xA0", /*#__PURE__*/React.createElement("a", {
+    }, " - ", this.props.txt('has been invalidated')), this.props.canInvalidate && /*#__PURE__*/React.createElement("span", null, ",\xA0", /*#__PURE__*/React.createElement("a", {
       className: "file-metainfo",
       href: '?invalidate=' + this.props.full_path,
       "data-file": this.props.full_path,
       onClick: this.handleInvalidate
-    }, "force file invalidation"))));
+    }, this.props.txt('force file invalidation')))));
   }
 
 }
@@ -1386,7 +1437,7 @@ class IgnoredFiles extends React.Component {
     }
 
     if (this.props.allFiles.length === 0) {
-      return /*#__PURE__*/React.createElement("p", null, "No files have been ignored via ", /*#__PURE__*/React.createElement("i", null, "opcache.blacklist_filename"));
+      return /*#__PURE__*/React.createElement("p", null, this.props.txt('No files have been ignored via <i>opcache.blacklist_filename</i>'));
     }
 
     const {
@@ -1395,15 +1446,16 @@ class IgnoredFiles extends React.Component {
     const offset = (currentPage - 1) * this.props.perPageLimit;
     const filesInPage = this.doPagination ? this.props.allFiles.slice(offset, offset + this.props.perPageLimit) : this.props.allFiles;
     const allFilesTotal = this.props.allFiles.length;
-    return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("h3", null, allFilesTotal, " ignore file locations"), this.doPagination && /*#__PURE__*/React.createElement(Pagination, {
+    return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("h3", null, this.props.txt('{0} ignore file locations', allFilesTotal)), this.doPagination && /*#__PURE__*/React.createElement(Pagination, {
       totalRecords: allFilesTotal,
       pageLimit: this.props.perPageLimit,
       pageNeighbours: 2,
       onPageChanged: this.onPageChanged,
-      refresh: this.state.refreshPagination
+      refresh: this.state.refreshPagination,
+      txt: this.props.txt
     }), /*#__PURE__*/React.createElement("table", {
       className: "tables ignored-list-table"
-    }, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", null, "Path"))), /*#__PURE__*/React.createElement("tbody", null, filesInPage.map((file, index) => {
+    }, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", null, this.props.txt('Path')))), /*#__PURE__*/React.createElement("tbody", null, filesInPage.map((file, index) => {
       return /*#__PURE__*/React.createElement("tr", {
         key: file
       }, /*#__PURE__*/React.createElement("td", null, file));
@@ -1435,7 +1487,7 @@ class PreloadedFiles extends React.Component {
     }
 
     if (this.props.allFiles.length === 0) {
-      return /*#__PURE__*/React.createElement("p", null, "No files have been preloaded ", /*#__PURE__*/React.createElement("i", null, "opcache.preload"));
+      return /*#__PURE__*/React.createElement("p", null, this.props.txt('No files have been preloaded <i>opcache.preload</i>'));
     }
 
     const {
@@ -1444,15 +1496,16 @@ class PreloadedFiles extends React.Component {
     const offset = (currentPage - 1) * this.props.perPageLimit;
     const filesInPage = this.doPagination ? this.props.allFiles.slice(offset, offset + this.props.perPageLimit) : this.props.allFiles;
     const allFilesTotal = this.props.allFiles.length;
-    return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("h3", null, allFilesTotal, " preloaded files"), this.doPagination && /*#__PURE__*/React.createElement(Pagination, {
+    return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("h3", null, this.props.txt('{0} preloaded files', allFilesTotal)), this.doPagination && /*#__PURE__*/React.createElement(Pagination, {
       totalRecords: allFilesTotal,
       pageLimit: this.props.perPageLimit,
       pageNeighbours: 2,
       onPageChanged: this.onPageChanged,
-      refresh: this.state.refreshPagination
+      refresh: this.state.refreshPagination,
+      txt: this.props.txt
     }), /*#__PURE__*/React.createElement("table", {
       className: "tables preload-list-table"
-    }, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", null, "Path"))), /*#__PURE__*/React.createElement("tbody", null, filesInPage.map((file, index) => {
+    }, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", null, this.props.txt('Path')))), /*#__PURE__*/React.createElement("tbody", null, filesInPage.map((file, index) => {
       return /*#__PURE__*/React.createElement("tr", {
         key: file
       }, /*#__PURE__*/React.createElement("td", null, file));
@@ -1595,24 +1648,24 @@ class Pagination extends React.Component {
         }, /*#__PURE__*/React.createElement("a", {
           className: "page-link",
           href: "#",
-          "aria-label": "Previous",
+          "aria-label": this.props.txt('Previous'),
           onClick: this.handleJumpLeft
         }, /*#__PURE__*/React.createElement("span", {
           "aria-hidden": "true"
         }, "\u219E"), /*#__PURE__*/React.createElement("span", {
           className: "sr-only"
-        }, "Jump back"))), /*#__PURE__*/React.createElement("li", {
+        }, this.props.txt('Jump back')))), /*#__PURE__*/React.createElement("li", {
           className: "page-item arrow"
         }, /*#__PURE__*/React.createElement("a", {
           className: "page-link",
           href: "#",
-          "aria-label": "Previous",
+          "aria-label": this.props.txt('Previous'),
           onClick: this.handleMoveLeft
         }, /*#__PURE__*/React.createElement("span", {
           "aria-hidden": "true"
         }, "\u21E0"), /*#__PURE__*/React.createElement("span", {
           className: "sr-only"
-        }, "Previous page"))));
+        }, this.props.txt('Previous page')))));
       }
 
       if (page === "RIGHT") {
@@ -1623,24 +1676,24 @@ class Pagination extends React.Component {
         }, /*#__PURE__*/React.createElement("a", {
           className: "page-link",
           href: "#",
-          "aria-label": "Next",
+          "aria-label": this.props.txt('Next'),
           onClick: this.handleMoveRight
         }, /*#__PURE__*/React.createElement("span", {
           "aria-hidden": "true"
         }, "\u21E2"), /*#__PURE__*/React.createElement("span", {
           className: "sr-only"
-        }, "Next page"))), /*#__PURE__*/React.createElement("li", {
+        }, this.props.txt('Next page')))), /*#__PURE__*/React.createElement("li", {
           className: "page-item arrow"
         }, /*#__PURE__*/React.createElement("a", {
           className: "page-link",
           href: "#",
-          "aria-label": "Next",
+          "aria-label": this.props.txt('Next'),
           onClick: this.handleJumpRight
         }, /*#__PURE__*/React.createElement("span", {
           "aria-hidden": "true"
         }, "\u21A0"), /*#__PURE__*/React.createElement("span", {
           className: "sr-only"
-        }, "Jump forward"))));
+        }, this.props.txt('Jump forward')))));
       }
 
       return /*#__PURE__*/React.createElement("li", {
@@ -1708,7 +1761,8 @@ function debounce(func, wait, immediate) {
         highlight: <?= json_encode($opcache->getOption('highlight')); ?>,
         debounceRate: <?= $opcache->getOption('debounce_rate'); ?>,
         perPageLimit: <?= json_encode($opcache->getOption('per_page')); ?>,
-        realtimeRefresh: <?= json_encode($opcache->getOption('refresh_time')); ?>
+        realtimeRefresh: <?= json_encode($opcache->getOption('refresh_time')); ?>,
+        language: <?= json_encode($opcache->getOption('language_pack')); ?>,
     }), document.getElementById('interface'));
 
     </script>

+ 7 - 3
src/Opcache/Service.php

@@ -50,6 +50,12 @@ class Service
      */
     public function __construct(array $options = [])
     {
+        $this->options = array_merge($this->defaults, $options);
+        $this->tz = new DateTimeZone(date_default_timezone_get());
+        if (is_string($this->options['language_pack'])) {
+            $this->options['language_pack'] = json_decode($this->options['language_pack'], true);
+        }
+
         $this->optimizationLevels = [
             1 << 0  => $this->txt('CSE, STRING construction'),
             1 << 1  => $this->txt('Constant conversion and jumps'),
@@ -107,8 +113,6 @@ class Service
                 ]
             ]
         ];
-        $this->options = array_merge($this->defaults, $options);
-        $this->tz = new DateTimeZone(date_default_timezone_get());
 
         $this->data = $this->compileState();
     }
@@ -120,7 +124,7 @@ class Service
     {
         $args = func_get_args();
         $text = array_shift($args);
-        if ((($lang = $this->getOption('language')) !== null) && !empty($lang[$text])) {
+        if ((($lang = $this->getOption('language_pack')) !== null) && !empty($lang[$text])) {
             $text = $lang[$text];
         }
         foreach ($args as $i => $arg) {