i18n implementation (#55)

This commit is contained in:
Athou
2013-05-12 12:38:56 +02:00
parent ca47270db1
commit 98aeccbb66
23 changed files with 353 additions and 124 deletions

View File

@@ -86,6 +86,7 @@
<extension>.class</extension>
</extensions>
<updateOnlyExtensions>
<updateOnlyExtension>.properties</updateOnlyExtension>
<updateOnlyExtension>.html</updateOnlyExtension>
<updateOnlyExtension>.js</updateOnlyExtension>
<updateOnlyExtension>.css</updateOnlyExtension>

View File

@@ -1,7 +1,11 @@
package com.commafeed.backend;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -14,6 +18,7 @@ import javax.ejb.Startup;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.mutable.MutableBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -27,6 +32,7 @@ import com.commafeed.backend.model.ApplicationSettings;
import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.services.UserService;
import com.google.api.client.util.Maps;
@Startup
@Singleton
@@ -59,6 +65,7 @@ public class StartupBean {
Instance<FeedRefreshWorker> workers;
private long startupTime;
private Map<String, String> supportedLanguages = Maps.newHashMap();
private ExecutorService executor;
private MutableBoolean running = new MutableBoolean(true);
@@ -70,6 +77,8 @@ public class StartupBean {
initialData();
}
initSupportedLanguages();
ApplicationSettings settings = applicationSettingsService.get();
int threads = settings.getBackgroundThreads();
log.info("Starting {} background threads", threads);
@@ -88,13 +97,30 @@ public class StartupBean {
}
private void initSupportedLanguages() {
Properties props = new Properties();
InputStream is = null;
try {
is = getClass().getResourceAsStream("/i18n/languages.properties");
props.load(new InputStreamReader(is, "UTF-8"));
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
IOUtils.closeQuietly(is);
}
for (Object key : props.keySet()) {
supportedLanguages.put(key.toString(),
props.getProperty(key.toString()));
}
}
private void initialData() {
log.info("Populating database with default values");
ApplicationSettings settings = new ApplicationSettings();
settings.setAnnouncement("Set the Public URL in the admin section !");
applicationSettingsService.save(settings);
userService.register(USERNAME_ADMIN, "admin",
Arrays.asList(Role.ADMIN, Role.USER));
userService.register(USERNAME_DEMO, "demo", Arrays.asList(Role.USER));
@@ -104,6 +130,10 @@ public class StartupBean {
return startupTime;
}
public Map<String, String> getSupportedLanguages() {
return supportedLanguages;
}
@PreDestroy
public void shutdown() {
running.setValue(false);

View File

@@ -1,11 +1,13 @@
package com.commafeed.frontend.model;
import java.io.Serializable;
import java.util.Map;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.google.api.client.util.Maps;
import com.wordnik.swagger.annotations.ApiClass;
@SuppressWarnings("serial")
@@ -15,6 +17,7 @@ import com.wordnik.swagger.annotations.ApiClass;
public class ServerInfo implements Serializable {
private String announcement;
private Map<String, String> supportedLanguages = Maps.newHashMap();
public String getAnnouncement() {
return announcement;
@@ -24,4 +27,12 @@ public class ServerInfo implements Serializable {
this.announcement = announcement;
}
public Map<String, String> getSupportedLanguages() {
return supportedLanguages;
}
public void setSupportedLanguages(Map<String, String> supportedLanguages) {
this.supportedLanguages = supportedLanguages;
}
}

View File

@@ -26,6 +26,7 @@ import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.util.crypt.Base64;
import com.commafeed.backend.MetricsBean;
import com.commafeed.backend.StartupBean;
import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.FeedEntryDAO;
@@ -89,6 +90,9 @@ public abstract class AbstractREST {
@Inject
UserService userService;
@Inject
StartupBean startupBean;
@Inject
UserSettingsDAO userSettingsDAO;

View File

@@ -18,6 +18,8 @@ public class ServerREST extends AbstractResourceREST {
ServerInfo infos = new ServerInfo();
infos.setAnnouncement(applicationSettingsService.get()
.getAnnouncement());
infos.getSupportedLanguages().putAll(
startupBean.getSupportedLanguages());
return infos;
}
}

View File

@@ -64,6 +64,10 @@ public class UserREST extends AbstractResourceREST {
public Response saveSettings(@ApiParam Settings settings) {
Preconditions.checkNotNull(settings);
if (startupBean.getSupportedLanguages().get(settings.getLanguage()) == null) {
settings.setLanguage("en");
}
UserSettings s = userSettingsDAO.findByUser(getUser());
if (s == null) {
s = new UserSettings();

View File

@@ -3,6 +3,7 @@ package com.commafeed.frontend.utils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.regex.Matcher;
@@ -67,9 +68,10 @@ public class InternationalizationDevelopmentFilter implements Filter {
String lang = settings.getLanguage() == null ? "en" : settings
.getLanguage();
byte[] bytes = translate(wrapper.toString(), lang).getBytes();
response.getOutputStream().write(bytes);
byte[] bytes = translate(wrapper.toString(), lang).getBytes("UTF-8");
response.setContentLength(bytes.length);
response.setCharacterEncoding("UTF-8");
response.getOutputStream().write(bytes);
response.getOutputStream().close();
}
@@ -80,7 +82,7 @@ public class InternationalizationDevelopmentFilter implements Filter {
try {
is = getClass()
.getResourceAsStream("/i18n/" + lang + ".properties");
props.load(is);
props.load(new InputStreamReader(is, "UTF-8"));
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
@@ -98,7 +100,9 @@ public class InternationalizationDevelopmentFilter implements Filter {
while (m.find()) {
String var = m.group(1);
Object replacement = props.get(var);
m.appendReplacement(sb, replacement.toString());
String replacementValue = replacement == null ? var : replacement
.toString();
m.appendReplacement(sb, replacementValue);
}
m.appendTail(sb);
return sb.toString();

View File

@@ -1 +1,88 @@
toolbar.refresh=Refresh
global.save=Save
global.cancel=Cancel
global.delete=Delete
global.required=Required
tree.subscribe=Subscribe
tree.import=Import
tree.new_category=New category
tree.all=All
tree.starred=Starred
subscribe.feed_url=Feed URL
subscribe.feed_name=Feed Name
subscribe.category=Category
import.google_reader_prefix=Let me import your feeds from your
import.google_reader_suffix= account.
import.google_download=Alternatively, upload your subscriptions.xml file.
import.google_download_link=Download it from here.
import.xml_file=XML File
new_category.name=Name
new_category.parent=Parent
toolbar.unread=Unread
toolbar.all=All
toolbar.refresh=Refresh
toolbar.mark_all_as_read=Mark all as read
toolbar.settings=Settings
toolbar.profile=Profile
toolbar.admin=Admin
toolbar.about=About
toolbar.logout=Logout
toolbar.donate=Donate
view.error_while_loading_feed=Error while loading this feed
view.keep_unread=Keep unread
view.no_unread_items=has no unread items.
settings.general=General
settings.general.language=Language
settings.general.show_unread=Show feeds and categories with no unread entries
settings.general.social_buttons=Show social sharing buttons
settings.general.scroll_marks=In expanded view, scrolling through entries mark them as read
settings.custom_css=Custom CSS
details.feed_details=Feed details
details.url=URL
details.name=Name
details.category=Category
details.last_refresh=Last refresh
details.feed_url=Feed URL
details.generate_api_key_first=Generate an API key in your profile first.
details.unsubscribe=Unsubscribe
details.category_details=Category details
details.parent_category=Parent category
profile.user_name=User name
profile.email=E-mail
profile.change_password=Change password
profile.confirm_password=Confirm password
profile.minimum_6_chars=Minimum 6 characters
profile.passwords_do_not_match=Passwords do not match
profile.api_key=API key
profile.api_key_not_generated=Not generated yet
profile.generate_new_api_key=Generate new API key
profile.generate_new_api_key_info=Changing password will generate a new API key
profile.delete_account=Delete account
about.rest_api=REST API
about.keyboard_shortcuts=Keyboard shortcuts
about.line1_prefix=CommaFeed is an open-source project. Sources are hosted on
about.line1_suffix=.
about.line2_prefix=If you encounter an issue, please report it on the issues page of the
about.line2_suffix= project.
about.line3=If you like this project, please consider a donation to support the developer and help cover the costs of keeping this website online.
about.rest_api.line1=CommaFeed is built on top of JAX-RS and AngularJS. As such, a REST API is available.
about.rest_api.link_to_documentation=Link to the documentation.
about.shortcuts.mouse_middleclick=mouse middleclick
about.shortcuts.open_next_entry=open next entry
about.shortcuts.open_previous_entry=open previous entry
about.shortcuts.open_close_current_entry=open/close current entry
about.shortcuts.open_current_entry_in_new_window=open current entry in a new window
about.shortcuts.star_unstar=star/unstar current entry
about.shortcuts.mark_current_entry=mark as read/unread current entry
about.shortcuts.mark_all_as_read=mark all entries as read
about.shortcuts.open_in_new_tab_mark_as_read=open entry in new tab and mark as read

View File

@@ -1 +1,88 @@
toolbar.refresh=Rafraichir
global.save=Enregistrer
global.cancel=Annuler
global.delete=Effacer
global.required=Obligatoire
tree.subscribe=S'abonner
tree.import=Importer
tree.new_category=Nouvelle catégorie
tree.all=Tous
tree.starred=Etoiles
subscribe.feed_url=URL du flux
subscribe.feed_name=Nom du flux
subscribe.category=Catégorie
import.google_reader_prefix=Laissez-moi importer vos flux depuis votre compte
import.google_reader_suffix=.
import.google_download=Ou alors, téléchargez votre fichier subscriptions.xml.
import.google_download_link=Récuperez-le ici.
import.xml_file=Fichier XML
new_category.name=Nom
new_category.parent=Parent
toolbar.unread=Non-lus
toolbar.all=Tous
toolbar.refresh=Rafraîchir
toolbar.mark_all_as_read=Tout marquer comme lu
toolbar.settings=Préférences
toolbar.profile=Profil
toolbar.admin=Administration
toolbar.about=A propos
toolbar.logout=Déconnexion
toolbar.donate=Faire un don
view.error_while_loading_feed=Erreur durant le chargement de ce flux
view.keep_unread=Garder non-lu
view.no_unread_items=n'a pas d'entrées non-lues.
settings.general=Général
settings.general.language=Langue
settings.general.show_unread=Afficher les flux et les catégories pour lesquels tout est déjà lu
settings.general.social_buttons=Afficher les boutons de partage sur réseaux sociaux
settings.general.scroll_marks=En mode de lecture étendu, marquer comme lu les éléments lorsque la fenêtre descend.
settings.custom_css=CSS personnelle
details.feed_details=Détails du flux
details.url=URL
details.name=Nom
details.category=Catégorie
details.last_refresh=Dernière mise à jour
details.feed_url=URL du flux
details.generate_api_key_first=Génerez une clé API dans votre profil d'abord.
details.unsubscribe=Se désabonner
details.category_details=Détails de la catégorie
details.parent_category=Catégorie parente
profile.user_name=Nom
profile.email=E-mail
profile.change_password=Changer de mot de passe
profile.confirm_password=Confirmer le mot de passe
profile.minimum_6_chars=Minimum 6 caractères
profile.passwords_do_not_match=Les mots de passe ne correspondent pas
profile.api_key=Clé API
profile.api_key_not_generated=Pas encore générée
profile.generate_new_api_key=Générer une nouvelle clé API
profile.generate_new_api_key_info=Changer de password va générer une nouvelle clé API
profile.delete_account=Effacer le compte
about.rest_api=API REST
about.keyboard_shortcuts=Raccourcis clavier
about.line1_prefix=CommaFeed est un projet open-source. Les sources sont disponibles sur
about.line1_suffix=.
about.line2_prefix=Si vous rencontrez un problème, rapportez-le sur la page du projet sur
about.line2_suffix=.
about.line3=Si vous aimez ce projet, n'hésitez pas à faire un don pour encourager le développeur et aider à couvrir les coûts d'hébergement de la plate-forme.
about.rest_api.line1=CommaFeed utilise JAX-RS et AngularJS et une API REST est donc disponible.
about.rest_api.link_to_documentation=Lien vers la documentation.
about.shortcuts.mouse_middleclick=click du milieu de la souris
about.shortcuts.open_next_entry=ouvrir l'entrée suivante
about.shortcuts.open_previous_entry=ouvrir l'entrée précédente
about.shortcuts.open_close_current_entry=ouvrir/fermer l'entrée courante
about.shortcuts.open_current_entry_in_new_window=ouvrir l'entrée courrnte dans une nouvelle fenêtre
about.shortcuts.star_unstar=Marquer/enlever l'étoile de l'entrée courante
about.shortcuts.mark_current_entry=Marquer comme lue/non-lue l'entrée cour-ante
about.shortcuts.mark_all_as_read=Marquer toutes les entrées comme lues
about.shortcuts.open_in_new_tab_mark_as_read=ouvrir l'entrée courante dans une nouvelle fenêtre et marquer comme lue

View File

@@ -0,0 +1,2 @@
en=English
fr=Français

View File

@@ -28,13 +28,13 @@ public class HTMLConcat {
}
sb.append("</div>");
String dest = destination.substring(0, destination.lastIndexOf(".")) + "." + i18n.getName().split("\\.")[0] + ".html";
FileUtils.writeStringToFile(new File(dest), sb.toString());
FileUtils.writeStringToFile(new File(dest), sb.toString(), "UTF-8");
}
}
public String translate(String content, File i18n) {
Properties props = new Properties();
props.load(new FileInputStream(i18n));
props.load(new InputStreamReader(new FileInputStream(i18n), "UTF-8"));
return replace(content, props);
}

View File

@@ -793,19 +793,12 @@ function($scope, $state, $stateParams, $dialog, AdminUsersService) {
};
}]);
module.controller('SettingsCtrl', ['$scope', '$location', 'SettingsService', 'AnalyticsService',
function($scope, $location, SettingsService, AnalyticsService) {
module.controller('SettingsCtrl', ['$scope', '$location', 'SettingsService', 'AnalyticsService', 'ServerService',
function($scope, $location, SettingsService, AnalyticsService, ServerService) {
AnalyticsService.track();
$scope.languages = [ {
id : 'en',
label : 'English'
}, {
id : 'fr',
label : 'Français'
} ];
$scope.ServerService = ServerService.get();
$scope.settingsService = SettingsService;
$scope.$watch('settingsService.settings', function(value) {

View File

@@ -34,6 +34,7 @@ module.factory('SettingsService', ['$resource', function($resource) {
s.init = function(callback) {
res.get(function(data) {
s.settings = data;
moment.lang(s.settings.language || 'en');
if (callback) {
callback(data);
}

View File

@@ -1,25 +1,25 @@
<dl class="dl-horizontal">
<dt>k</dt>
<dd>open next entry</dd>
<dd>${about.shortcuts.open_next_entry}</dd>
<dt>j</dt>
<dd>open previous entry</dd>
<dd>${about.shortcuts.open_previous_entry}</dd>
<dt>o, enter</dt>
<dd>open/close current entry</dd>
<dd>${about.shortcuts.open_close_current_entry}</dd>
<dt>v</dt>
<dd>open current entry in a new window</dd>
<dd>${about.shortcuts.open_current_entry_in_new_window}</dd>
<dt>s</dt>
<dd>star/unstar current entry</dd>
<dd>${about.shortcuts.star_unstar}</dd>
<dt>m</dt>
<dd>mark as read/unread current entry</dd>
<dd>${about.shortcuts.mark_current_entry}</dd>
<dt>shift+a</dt>
<dd>mark all entries as read</dd>
<dd>${about.shortcuts.mark_all_as_read}</dd>
<dt>mouse middleclick</dt>
<dd>open entry in new tab and mark as read</dd>
<dt>${about.shortcuts.mouse_middleclick}</dt>
<dd>${about.shortcuts.open_in_new_tab_mark_as_read}</dd>
</dl>

View File

@@ -1,109 +1,109 @@
<div ng-controller="SubscribeCtrl">
<div class="btn-group">
<button class="btn" ng-click="open()"><span class="icon-rss"></span> Subscribe</button>
<button class="btn" ng-click="open()"><span class="icon-rss"></span> ${tree.subscribe}</button>
<button class="btn dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a ng-click="openImport()"><i class="icon-arrow-down"></i> Import</a></li>
<li><a ng-click="openCategory()"><i class="icon-plus"></i> Add a category</a></li>
<li><a ng-click="openImport()"><i class="icon-arrow-down"></i> ${tree.import}</a></li>
<li><a ng-click="openCategory()"><i class="icon-plus"></i> ${tree.new_category}</a></li>
</ul>
</div>
<div modal="isOpen" close="close()" options="opts">
<div class="modal-header">
<button type="button" class="close" ng-click="close()">&times;</button>
<h4>Subscribe</h4>
<h4>${tree.subscribe}</h4>
</div>
<form name="subscribeForm" class="form-horizontal" ng-submit="save()">
<div class="modal-body">
<div class="control-group" ng-class="{error : !subscribeForm.url.$valid}">
<label class="control-label">Feed URL</label>
<label class="control-label">${subscribe.feed_url}</label>
<div class="controls">
<input type="text" name="url" ng-model="sub.url" ng-blur="urlChanged()" placeholder="http://example.com/feed"
class="input-block-level" required ng-disabled="state=='loading'"></input>
<span class="help-block" ng-show="!subscribeForm.url.$valid">Required</span>
<span class="help-block" ng-show="!subscribeForm.url.$valid">${global.required}</span>
</div>
</div>
<div class="control-group" ng-class="{error : !subscribeForm.title.$valid}">
<label class="control-label">Feed Name</label>
<label class="control-label">${subscribe.feed_name}</label>
<div class="controls">
<input type="text" name="title" ng-model="sub.title"
class="input-block-level" required ng-disabled="state=='loading'"></input>
<span class="help-block" ng-show="!subscribeForm.title.$valid">Required</span>
<span class="help-block" ng-show="!subscribeForm.title.$valid">${global.required}</span>
</div>
</div>
<div class="control-group" ng-class="{error : !subscribeForm.category.$valid}">
<label class="control-label">Category</label>
<label class="control-label">${subscribe.category}</label>
<div class="controls">
<select name="category" ng-model="sub.categoryId" class="input-block-level"
ng-options="cat.id as cat.name for cat in CategoryService.flatCategories" required>
</select>
<span class="help-block" ng-show="!subscribeForm.category.$valid">Required</span>
<span class="help-block" ng-show="!subscribeForm.category.$valid">${global.required}</span>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn cancel" ng-click="close()" type="button">Cancel</button>
<button class="btn btn-primary ok" type="submit" ng-disabled="state!='ok'">Save</button>
<button class="btn cancel" ng-click="close()" type="button">${global.cancel}</button>
<button class="btn btn-primary ok" type="submit" ng-disabled="state!='ok'">${global.save}</button>
</div>
</form>
</div>
<div modal="isOpenImport" close="closeImport()" options="opts">
<div class="modal-header">
<button type="button" class="close" ng-click="closeImport()">&times;</button>
<h4>Import</h4>
<h4>${tree.import}</h4>
</div>
<form ng-upload class="form-horizontal" action="rest/feed/import">
<div class="modal-body">
<div class="control-group">
<div>Let me import your feeds from your
<div>${import.google_reader_prefix}
<a href="google/import/redirect">
<img src="images/google_reader_icon.png" class="favicon" />
Google Reader account
</a>.
Google Reader
</a>${import.google_reader_suffix}
</div>
<div>Alternatively, upload your subscriptions.xml file (download it from <a target="_blank" href="https://www.google.com/reader/subscriptions/export">here)</a>.</div>
<div>${import.google_download} <a target="_blank" href="https://www.google.com/reader/subscriptions/export">${import.google_download_link}</a></div>
</div>
<div class="control-group">
<label class="control-label">XML File</label>
<label class="control-label">${import.xml_file}</label>
<div class="controls">
<input type="file" name="file"></input>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn cancel" ng-click="closeImport()">Cancel</button>
<button class="btn btn-primary ok" type="submit" upload-submit="uploadComplete(contents, completed)">Import</button>
<button type="button" class="btn cancel" ng-click="closeImport()">${global.cancel}</button>
<button class="btn btn-primary ok" type="submit" upload-submit="uploadComplete(contents, completed)">${tree.import}</button>
</div>
</form>
</div>
<div modal="isOpenCategory" close="closeCategory()" options="opts">
<div class="modal-header">
<button type="button" class="close" ng-click="closeCategory()">&times;</button>
<h4>New category</h4>
<h4>${tree.new_category}</h4>
</div>
<form name="categoryForm" class="form-horizontal" ng-submit="saveCategory()">
<div class="modal-body">
<div class="control-group" ng-class="{error : !categoryForm.name.$valid}">
<label class="control-label">Name</label>
<label class="control-label">${new_category.name}</label>
<div class="controls">
<input type="text" name="name" ng-model="cat.name" required></input>
<span class="help-block" ng-show="!categoryForm.name.$valid">Required</span>
<span class="help-block" ng-show="!categoryForm.name.$valid">${global.required}</span>
</div>
</div>
<div class="control-group" ng-class="{error : !categoryForm.category.$valid}">
<label class="control-label">Parent</label>
<label class="control-label">${new_category.parent}</label>
<div class="controls">
<select name="category" ng-model="cat.parentId" class="input-block-level"
ng-options="cat.id as cat.name for cat in CategoryService.flatCategories"required>
</select>
<span class="help-block" ng-show="!categoryForm.category.$valid">Required</span>
<span class="help-block" ng-show="!categoryForm.category.$valid">${global.required}</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn cancel" ng-click="closeCategory()">Cancel</button>
<button class="btn btn-primary ok" type="submit">Save</button>
<button type="button" class="btn cancel" ng-click="closeCategory()">${global.cancel}</button>
<button class="btn btn-primary ok" type="submit">${global.save}</button>
</div>
</form>
</div>

View File

@@ -2,8 +2,8 @@
<div class="form-horizontal">
<span>
<div class="btn-group read-mode" data-toggle="buttons-radio">
<button type="button" class="btn" ng-model="settingsService.settings.readingMode" btn-radio="'unread'">Unread</button>
<button type="button" class="btn" ng-model="settingsService.settings.readingMode" btn-radio="'all'">All</button>
<button type="button" class="btn" ng-model="settingsService.settings.readingMode" btn-radio="'unread'">${toolbar.unread}</button>
<button type="button" class="btn" ng-model="settingsService.settings.readingMode" btn-radio="'all'">${toolbar.all}</button>
</div>
<button type="button" class="btn" ng-click="toggleOrder()">
@@ -16,19 +16,19 @@
</div>
<button type="button" class="btn" ng-click="refresh()"><i class="icon-refresh"></i> ${toolbar.refresh}</button>
<button type="button" class="btn" ng-click="markAllAsRead()"><i class="icon-ok"></i> Mark all as read</button>
<button type="button" class="btn" ng-click="markAllAsRead()"><i class="icon-ok"></i> ${toolbar.mark_all_as_read}</button>
<div class="btn-group">
<a class="btn" ng-click="toSettings()"><i class="icon-cog"></i> Settings</a>
<a class="btn" ng-click="toSettings()"><i class="icon-cog"></i> ${toolbar.settings}</a>
<button class="btn dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
</button>
<ul class="dropdown-menu pull-right">
<li><a ng-click="toProfile()"><i class="icon-user"></i> Profile</a></li>
<li ng-show="session.admin"><a ng-click="toAdmin()"><i class="icon-edit"></i> Admin</a></li>
<li><a ng-click="toProfile()"><i class="icon-user"></i> ${toolbar.profile}</a></li>
<li ng-show="session.admin"><a ng-click="toAdmin()"><i class="icon-edit"></i> ${toolbar.admin}</a></li>
<li class="divider"></li>
<li><a ng-click="toHelp()"><i class="icon-question-sign"></i> About</a></li>
<li><a href="logout"><i class="icon-off"></i> Logout</a></li>
<li><a ng-click="toHelp()"><i class="icon-question-sign"></i> ${toolbar.about}</a></li>
<li><a href="logout"><i class="icon-off"></i> ${toolbar.logout}</a></li>
</ul>
</div>
</span>
@@ -37,7 +37,7 @@
<button class="btn" type="submit"><i class="icon-search"></i></button>
</form>
<div class="donate">
<button class="btn btn-success" type="button" ng-click="toDonate()">Donate</button>
<button class="btn btn-success" type="button" ng-click="toDonate()">${toolbar.donate}</button>
</div>
<div spinner shown="loading"></div>
<span>{{ServerService.announcement}}</span>

View File

@@ -1,38 +1,38 @@
<div>
<div class="page-header">
<h3>Category details</h3>
<h3>${details.category_details}</h3>
</div>
<form name="form" class="form-horizontal" ng-submit="save()">
<div class="control-group" ng-class="{error : !form.name.$valid}" ui-if="!isMeta()">
<label class="control-label">Name</label>
<label class="control-label">${details.name}</label>
<div class="controls">
<input type="text" name="name" ng-model="category.name" class="input-block-level" required></input>
<span class="help-block" ng-show="!form.name.$valid">Required</span>
<span class="help-block" ng-show="!form.name.$valid">${global.required}</span>
</div>
</div>
<div class="control-group" ng-class="{error : !form.category.$valid}" ui-if="!isMeta()">
<label class="control-label">Parent category</label>
<label class="control-label">${details.parent_category}</label>
<div class="controls">
<select name="category" class="input-block-level" ng-model="category.parentId"
ng-options="cat.id as cat.name for cat in CategoryService.flatCategories | filter: filterCurrent">
</select>
<span class="help-block" ng-show="!form.category.$valid">Required</span>
<span class="help-block" ng-show="!form.category.$valid">${global.required}</span>
</div>
</div>
<div class="control-group">
<label class="control-label">Feed URL</label>
<label class="control-label">${details.feed_url}</label>
<div class="controls horizontal-align">
<a ng-show="user.apiKey" href="{{'rest/category/entriesAsFeed?id=' + category.id + '&apiKey=' + user.apiKey}}" target="_blank">link</a>
<span ng-show="!user.apiKey">Generate an API key in your profile first.</span>
<span ng-show="!user.apiKey">${details.generate_api_key_first}</span>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" ui-if="!isMeta()">Save</button>
<button type="button" class="btn" ng-click="back()">Cancel</button>
<button type="button" class="btn btn-danger" ng-click="deleteCategory()" ui-if="!isMeta()">Delete</button>
<button type="submit" class="btn btn-primary" ui-if="!isMeta()">${global.save}</button>
<button type="button" class="btn" ng-click="back()">${global.cancel}</button>
<button type="button" class="btn btn-danger" ng-click="deleteCategory()" ui-if="!isMeta()">${global.delete}</button>
</div>
</form>

View File

@@ -1,51 +1,51 @@
<div>
<div class="page-header">
<h3>Feed details</h3>
<h3>${details.feed_details}</h3>
</div>
<form name="form" class="form-horizontal" ng-submit="save()">
<div class="control-group">
<label class="control-label">URL</label>
<label class="control-label">${details.url}</label>
<div class="controls horizontal-align">
<a href="{{sub.feedUrl}}" target="_blank">{{sub.feedUrl}}</a>
</div>
</div>
<div class="control-group" ng-class="{error : !form.name.$valid}">
<label class="control-label">Name</label>
<label class="control-label">${details.name}</label>
<div class="controls">
<input type="text" name="name" ng-model="sub.name" class="input-block-level" required></input>
<span class="help-block" ng-show="!form.name.$valid">Required</span>
<span class="help-block" ng-show="!form.name.$valid">${global.required}</span>
</div>
</div>
<div class="control-group" ng-class="{error : !form.category.$valid}">
<label class="control-label">Category</label>
<label class="control-label">${details.category}</label>
<div class="controls">
<select name="category" class="input-block-level" ng-model="sub.categoryId"
ng-options="cat.id as cat.name for cat in CategoryService.flatCategories">
</select>
<span class="help-block" ng-show="!form.category.$valid">Required</span>
<span class="help-block" ng-show="!form.category.$valid">${global.required}</span>
</div>
</div>
<div class="control-group">
<label class="control-label">Last refresh</label>
<label class="control-label">${details.last_refresh}</label>
<div class="controls horizontal-align">
<span>{{sub.lastRefresh|entryDate}}</span>
</div>
</div>
<div class="control-group">
<label class="control-label">Feed URL</label>
<label class="control-label">${details.feed_url}</label>
<div class="controls horizontal-align">
<a ng-show="user.apiKey" href="{{'rest/feed/entriesAsFeed?id=' + sub.id + '&apiKey=' + user.apiKey}}" target="_blank">link</a>
<span ng-show="!user.apiKey">Generate an API key in your profile first.</span>
<span ng-show="!user.apiKey">${details.generate_api_key_first}</span>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn" ng-click="back()">Cancel</button>
<button type="button" class="btn btn-danger" ng-click="unsubscribe()">Unsubscribe</button>
<button type="submit" class="btn btn-primary">${global.save}</button>
<button type="button" class="btn" ng-click="back()">${global.cancel}</button>
<button type="button" class="btn btn-danger" ng-click="unsubscribe()">${details.unsubscribe}</button>
</div>
</form>

View File

@@ -2,28 +2,28 @@
<div class="span2">
<ul class="nav nav-pills nav-stacked">
<li class="active">
<a href="#about" data-toggle="pill">About</a>
<a href="#about" data-toggle="pill">${toolbar.about}</a>
</li>
<li>
<a href="#api" data-toggle="pill">REST API</a>
<a href="#api" data-toggle="pill">${about.rest_api}</a>
</li>
<li>
<a href="#shortcuts" data-toggle="pill">Keyboard shortcuts</a>
<a href="#shortcuts" data-toggle="pill">${about.keyboard_shortcuts}</a>
</li>
</ul>
</div>
<div class="span10">
<div class="tab-content">
<div class="tab-pane active" id="about">
<h3>About</h3>
<h3>${toolbar.about}</h3>
<p>
CommaFeed is an open-source project. Sources are hosted on <a href="https://github.com/Athou/commafeed" target="_blank">GitHub</a>.
${about.line1_prefix} <a href="https://github.com/Athou/commafeed" target="_blank">GitHub</a> ${about.line1_suffix}
</p>
<p>
If you encounter an issue, please report it on the <a href="https://github.com/Athou/commafeed/issues" target="_blank">issues page</a> of the GitHub project.
${about.line2_prefix} <a href="https://github.com/Athou/commafeed/issues" target="_blank">GitHub</a> ${about.line2_suffix}
</p>
<p>
If you like this project, please consider a donation to support the developer and help cover the costs of keeping this website online.
${about.line3}
</p>
<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top">
<input type="hidden" name="cmd" value="_s-xclick">
@@ -35,16 +35,16 @@
</div>
<div class="tab-pane" id="api">
<h3>REST API</h3>
<h3>${about.rest_api}</h3>
<p>
CommaFeed is built on top of JAX-RS and AngularJS. As such, a REST API is available.
${about.rest_api.line1}
</p>
<p>
The documentation about the API is available <a href="api" target="_blank">here</a>.
<a href="api" target="_blank">${about.rest_api.link_to_documentation}</a>
</p>
</div>
<div class="tab-pane" id="shortcuts">
<h3>Keyboard Shortcuts</h3>
<h3>${about.keyboard_shortcuts}</h3>
<div ng-include="'templates/_shortcuts.html'"></div>
</div>
</div>

View File

@@ -5,7 +5,7 @@
<div infinite-scroll="loadMoreEntries()" infinite-scroll-disabled="busy || !settingsService.settings.readingMode" infinite-scroll-distance="1" id="feed-accordion"
ng-class="{'expanded' : settingsService.settings.viewMode == 'expanded' }">
<div ng-show="message && errorCount > 10">Error while loading this feed : {{message}}</div>
<div ng-show="message && errorCount > 10">${view.error_while_loading_feed} : {{message}}</div>
<div ng-repeat="entry in entries" class="entry" scroll-to="navigationMode == 'click' && isOpen && current == entry" scroll-to-offset="-58"
on-scroll-middle="onScroll(entry)" ng-class="{current: current==entry}">
<a href="{{entry.url}}" target="_blank" class="entry-heading" ng-click="noop($event)" ng-mouseup="entryClicked(entry, $event)"
@@ -69,11 +69,11 @@
</div>
</div>
</div>
<div class="no-entries" ng-show="name && entries.length == 0 && !busy">"{{name}}" has no unread items.</div>
<div class="no-entries" ng-show="name && entries.length == 0 && !busy">"{{name}}" ${view.no_unread_items}</div>
<div modal="shortcutsModal" close="shortcutsModal=false" options="shortcutsOpts">
<div class="modal-header">
<button type="button" class="close" ng-click="shortcutsModal=false">&times;</button>
<h4>Keyboard shortcuts</h4>
<h4>${about.keyboard_shortcuts}</h4>
</div>
<div ng-include="'templates/_shortcuts.html'"></div>
</div>

View File

@@ -1,56 +1,56 @@
<div class="container-fluid profile">
<div class="page-header">
<h1>Profile</h1>
<h1>${toolbar.profile}</h1>
</div>
<form name="profileForm" ng-submit="save()" class="form-horizontal">
<div class="control-group">
<label class="control-label" for="email">User name</label>
<label class="control-label" for="email">${profile.user_name}</label>
<div class="controls horizontal-align">
<span>{{user.name}}</span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="email">E-mail</label>
<label class="control-label" for="email">${profile.email}</label>
<div class="controls">
<input type="email" id="email" ng-model="user.email" />
</div>
</div>
<div class="control-group">
<label class="control-label" for="password">Change password</label>
<label class="control-label" for="password">${profile.change_password}</label>
<div class="controls">
<input type="password" name="password" id="password" ng-model="user.password"
ng-minlength="6" />
<span class="help-inline" ng-show="profileForm.password.$error.minlength">Minimum 6 characters</span>
<span class="help-inline" ng-show="profileForm.password.$error.minlength">${profile.minimum_6_chars}</span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="password">Confirm password</label>
<label class="control-label" for="password">${profile.confirm_password}</label>
<div class="controls">
<input type="password" name="password_c" id="password_c" ng-model="password_c"
ui-validate="'$value==user.password'" ui-validate-watch="'user.password'">
<span class="help-inline" ng-show="profileForm.password_c.$error.validator">Passwords do not match</span>
<span class="help-inline" ng-show="profileForm.password_c.$error.validator">${profile.passwords_do_not_match}</span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="password">API key</label>
<label class="control-label" for="password">${profile.api_key}</label>
<div class="controls horizontal-align">
<span ng-show="user.apiKey">{{user.apiKey}}</span>
<span ng-show="!user.apiKey">Not generated yet</span>
<span ng-show="!user.apiKey">${profile.api_key_not_generated}</span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="password">Generate new API key</label>
<label class="control-label" for="password">${profile.generate_new_api_key}</label>
<div class="controls">
<input type="checkbox" name="newApiKey" id="newApiKey" ng-model="newApiKey">
<span class="help-block">Changing password will generate a new API key</span>
<span class="help-block">${profile.generate_new_api_key_info}</span>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn" ng-click="cancel()">Cancel</button>
<button type="button" class="btn btn-danger" ng-click="deleteAccount()">Delete account</button>
<button type="submit" class="btn btn-primary">${global.save}</button>
<button type="button" class="btn" ng-click="cancel()">${global.cancel}</button>
<button type="button" class="btn btn-danger" ng-click="deleteAccount()">${profile.delete_account}</button>
</div>
</form>
</div>

View File

@@ -7,11 +7,11 @@
<div class="span2">
<ul class="nav nav-pills nav-stacked">
<li class="active">
<a href="#general" data-toggle="pill">General</a>
<a href="#general" data-toggle="pill">${settings.general}</a>
</li>
<li>
<a href="#custom-css" data-toggle="pill"
ng-click="refreshCodemirror=!refreshCodemirror">Custom CSS</a>
ng-click="refreshCodemirror=!refreshCodemirror">${settings.custom_css}</a>
</li>
</ul>
</div>
@@ -20,10 +20,10 @@
<div class="tab-pane active" id="general">
<div class="form-horizontal">
<div class="control-group">
<label class="control-label">Language</label>
<label class="control-label">${settings.general.language}</label>
<div class="controls">
<select name="language" ng-model="settings.language" class="input-block-level"
ng-options="lang.id as lang.label for lang in languages" required>
ng-options="id as label for (id,label) in ServerService.supportedLanguages" required>
</select>
</div>
</div>
@@ -31,21 +31,21 @@
<label class="checkbox">
<input type="checkbox" name="showRead"
ng-model="settings.showRead" />
Show feeds and categories with no unread entries
${settings.general.show_unread}
</label>
</div>
<div class="control-group">
<label class="checkbox">
<input type="checkbox" name="socialButtons"
ng-model="settings.socialButtons" />
Show social sharing buttons
${settings.general.social_buttons}
</label>
</div>
<div class="control-group">
<label class="checkbox">
<input type="checkbox" name="scrollMarks"
ng-model="settings.scrollMarks" />
In expanded view, scrolling through entries mark them as read
${settings.general.scroll_marks}
</label>
</div>
</div>
@@ -58,8 +58,8 @@
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn" ng-click="cancel()">Cancel</button>
<button type="submit" class="btn btn-primary">${global.save}</button>
<button type="button" class="btn" ng-click="cancel()">${global.cancel}</button>
</div>
</form>
</div>

File diff suppressed because one or more lines are too long