diff --git a/README.md b/README.md new file mode 100644 index 000000000..d8295838f --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# FrontlineSMS v2 +## What is FrontlineSMS v2? +[FrontlineSMS](http://www.frontlinesms.com) is desktop/cloud based software created to lower barriers to positive social change using mobile technology. Its second version is built as a [Grails](http://grails.org/) app, which can either run stand-alone as a desktop app, or be bundled as the 'frontlinesms-core-multitenant' plugin that forms the core of [FrontlineCloud](http://cloud.frontlinesms.com). + +## Repository layout +In the plugins folder, you will find: +- *frontlinesms-core*, the Grails core codebase for FrontlineSMS v2 +- *radio*, the Grails codebase for [FrontlineSMS:Radio](http://www.frontlinesms.com/technologies/beta-products/), which uses frontlinesms-core as a plugin + +This repository is updated on every public release of FrontlineSMS v2. + +## Useful reading +- Overviews of [FrontlineSMS](http://www.frontlinesms.com/technologies/frontlinesms-overview/) and [FrontlineCloud](http://www.frontlinesms.com/technologies/frontlinecloud-overview/) +- [Grails documentation](https://grails.org/) +- [FrontlineSMS developers google group](https://groups.google.com/forum/#!forum/frontlinesms-developers) +-Just testing \ No newline at end of file diff --git a/application.properties b/application.properties deleted file mode 100644 index 463abff39..000000000 --- a/application.properties +++ /dev/null @@ -1,17 +0,0 @@ -#Grails Metadata file -#Tue Feb 28 10:58:01 EAT 2012 -app.grails.version=1.3.7 -app.name=frontlinesms2 -app.servlet.version=2.4 -app.version=0.b1-SNAPSHOT -plugins.code-coverage=1.2 -plugins.codenarc=0.15 -plugins.csv=0.3.1 -plugins.export=1.0 -plugins.hibernate=1.3.7 -plugins.jquery=1.6.1.1 -plugins.jquery-ui=1.8.11 -plugins.quartz=0.4.2 -plugins.routing=1.1.2-frontlinesms -plugins.tomcat=1.3.7 -plugins.webxml=1.4.1 diff --git a/grails-app/conf/BootStrap.groovy b/grails-app/conf/BootStrap.groovy deleted file mode 100644 index 311ed86d2..000000000 --- a/grails-app/conf/BootStrap.groovy +++ /dev/null @@ -1,24 +0,0 @@ -import grails.util.Environment -class BootStrap { - def grailsApplication - - def init = { servletContext -> - - switch(Environment.current) { - case Environment.TEST: - break - - case Environment.DEVELOPMENT: - //DB Viewer - //org.hsqldb.util.DatabaseManager.main() - // do custom init for dev here - if(System.properties['radio.plugin']) { - grailsApplication.config.frontlinesms2.plugin = "radio" - } - break - } - } - - def destroy = { - } -} diff --git a/grails-app/conf/BuildConfig.groovy b/grails-app/conf/BuildConfig.groovy deleted file mode 100644 index b7b7942d7..000000000 --- a/grails-app/conf/BuildConfig.groovy +++ /dev/null @@ -1,89 +0,0 @@ -grails.project.class.dir = "target/classes" -grails.project.test.class.dir = "target/test-classes" -grails.project.test.reports.dir = "target/test-reports" -//grails.project.war.file = "target/${appName}-${appVersion}.war" - -if(Boolean.parseBoolean(System.properties['radio.plugin'])) { - println "Loading radio plugin" - grails.plugin.location.radio = "plugins/radio" -} else { - println "Loading core plugin" - grails.plugin.location.core = "plugins/core" -} - - -grails.project.dependency.resolution = { - // Everything with a version that ends with -SNAPSHOT is changing -// chainResolver.changingPattern = '.*-SNAPSHOT' // This causes all snapshot dependencies to be looked for in remote repos - if(Boolean.parseBoolean(System.properties['snapshots'])) { - chainResolver.changingPattern = '.*-SNAPSHOT' // This causes all snapshot dependencies to be looked for in remote repos - } - - // inherit Grails' default dependencies - inherits("global") { - // uncomment to disable ehcache - // excludes 'ehcache' - } - log "warn" // log level of Ivy resolver, either 'error', 'warn', 'info', 'debug' or 'verbose' - repositories { - grailsHome() - - // uncomment the below to enable remote dependency resolution - // from public Maven repositories - mavenLocal() - mavenCentral() - //mavenRepo "http://snapshots.repository.codehaus.org" - //mavenRepo "http://repository.codehaus.org" - //mavenRepo "http://download.java.net/maven/2/" - //mavenRepo "http://repository.jboss.com/maven2/" - mavenRepo "http://dev.frontlinesms.com/m2repo/" - mavenRepo "https://nexus.codehaus.org/content/repositories/snapshots/" - - grailsPlugins() - grailsCentral() - - if(Boolean.parseBoolean(System.properties['snapshots'])) { - // from https://github.com/alkemist/grails-snapshot-dependencies-fix - // Register the new JAR - def classLoader = getClass().classLoader - classLoader.addURL(new File(baseFile, "lib/grails-snapshot-dependencies-fix-0.1.jar").toURL()) - // Get a hold of the class for the new resolver - def snapshotResolverClass = classLoader.loadClass("grails.resolver.SnapshotAwareM2Resolver") - // Define a new resolver that is snapshot aware - resolver(snapshotResolverClass.newInstance(name: "spock-snapshots", root: "http://m2repo.spockframework.org/snapshots")) - } - } - dependencies { - // specify dependencies here under either 'build', 'compile', 'runtime', 'test' or 'provided' scopes eg. - - // SHOULD BE AVAILABLE ONLY IN DEV SCOPE - compile ('net.frontlinesms.test:hayescommandset-test:0.0.4') { - changing = true - } // doesn't seem to cause problems if it's here, but should really only be included for dev scope - - // COMPILE - compile 'net.frontlinesms.core:camel-smslib:0.0.2' - compile 'org.apache.camel:camel-mail:2.5.0' - compile 'net.frontlinesms.core:serial:1.0.1' - compile 'net.frontlinesms.core:at-modem-detector:0.1' - runtime 'org.rxtx:rxtx:2.1.7' - runtime 'javax.comm:comm:2.0.3' - } -} - -coverage { - xml = true - enabledByDefault = false -} - -codenarc.reports = { - MyXmlReport('xml') { - outputFile = 'target/test-reports/CodeNarcReport.xml' - title = 'FrontlineSMS2 CodeNarc Report (xml)' - } - - MyHtmlReport('html') { - outputFile = 'target/test-reports/CodeNarcReport.html' - title = 'FrontlineSMS2 CodeNarc Report (html)' - } -} diff --git a/grails-app/conf/Config.groovy b/grails-app/conf/Config.groovy deleted file mode 100644 index 80aac16e9..000000000 --- a/grails-app/conf/Config.groovy +++ /dev/null @@ -1,108 +0,0 @@ -// locations to search for config files that get merged into the main config -// config files can either be Java properties files or ConfigSlurper scripts - -// grails.config.locations = [ "classpath:${appName}-config.properties", -// "classpath:${appName}-config.groovy", -// "file:${userHome}/.grails/${appName}-config.properties", -// "file:${userHome}/.grails/${appName}-config.groovy"] - -// if(System.properties["${appName}.config.location"]) { -// grails.config.locations << "file:" + System.properties["${appName}.config.location"] -// } - -grails.project.groupId = appName // change this to alter the default package name and Maven publishing destination -grails.mime.file.extensions = true // enables the parsing of file extensions from URLs into the request format -grails.mime.use.accept.header = false -grails.mime.types = [ html: ['text/html','application/xhtml+xml'], - xml: ['text/xml', 'application/xml'], - text: 'text/plain', - js: 'text/javascript', - rss: 'application/rss+xml', - atom: 'application/atom+xml', - css: 'text/css', - csv: 'text/csv', - pdf: 'application/pdf', - all: '*/*', - json: ['application/json','text/json'], - form: 'application/x-www-form-urlencoded', - multipartForm: 'multipart/form-data' - ] - -// URL Mapping Cache Max Size, defaults to 5000 -//grails.urlmapping.cache.maxsize = 1000 - -// The default codec used to encode data with ${} -grails.views.default.codec = "none" // none, html, base64 -grails.views.gsp.encoding = "UTF-8" -grails.converters.encoding = "UTF-8" -// enable Sitemesh preprocessing of GSP pages -grails.views.gsp.sitemesh.preprocess = true -// scaffolding templates configuration -grails.scaffolding.templates.domainSuffix = 'Instance' - -// jquery plugin -grails.views.javascript.library = "jquery" - -//fronlinesms2 plugin -frontlinesms2.plugin = "core" - -// pagination -grails.views.pagination.max = 50 - -// Set to false to use the new Grails 1.2 JSONBuilder in the render method -grails.json.legacy.builder = false -// enabled native2ascii conversion of i18n properties files -grails.enable.native2ascii = true -// whether to install the java.util.logging bridge for sl4j. Disable for AppEngine! -grails.logging.jul.usebridge = true -// packages to include in Spring bean scanning -grails.spring.bean.packages = [] - -// request parameters to mask when logging exceptions -grails.exceptionresolver.params.exclude = ['password'] - -// set per-environment serverURL stem for creating absolute links -environments { - production { - grails.serverURL = "http://www.changeme.com" - } - development { - grails.serverURL = "http://localhost:8080/${appName}" - } - test { - grails.serverURL = "http://localhost:8080/${appName}" - } -} - -// log4j configuration -log4j = { - // Example of changing the log pattern for the default console - // appender: - // - appenders { - // console name:'stdout', layout:pattern(conversionPattern: '%c{2} %m%n') - rollingFile name:'trace-file', file:"${System.properties.'user.home'}/.frontlinesms2/frontlinesms2-trace.log" - } - - root { - trace 'trace-file' - additivity = true - } - - error 'org.codehaus.groovy.grails.web.servlet', // controllers - 'org.codehaus.groovy.grails.web.pages', // GSP - 'org.codehaus.groovy.grails.web.sitemesh', // layouts - 'org.codehaus.groovy.grails.web.mapping.filter', // URL mapping - 'org.codehaus.groovy.grails.web.mapping', // URL mapping - 'org.codehaus.groovy.grails.commons', // core / classloading - 'org.codehaus.groovy.grails.plugins', // plugins - 'org.codehaus.groovy.grails.orm.hibernate', // hibernate integration - 'org.springframework', - 'org.hibernate', - 'net.sf.ehcache.hibernate' - - warn 'org.mortbay.log' - - info 'serial', - 'org.apache.camel' -} diff --git a/grails-app/conf/DataSource.groovy b/grails-app/conf/DataSource.groovy deleted file mode 100644 index 85308b9f5..000000000 --- a/grails-app/conf/DataSource.groovy +++ /dev/null @@ -1,47 +0,0 @@ -dataSource { - pooled = true - driverClassName = "org.hsqldb.jdbcDriver" - username = "sa" - password = "" -} -// environment specific settings -hibernate { - cache.use_second_level_cache = true - cache.use_query_cache = true - cache.provider_class = 'net.sf.ehcache.hibernate.EhCacheProvider' -} - - -environments { - development { - dataSource { - dbCreate = "create-drop" // one of 'create', 'create-drop','update' - url = "jdbc:hsqldb:mem:devDB" - } - } - - test { - dataSource { - dbCreate = "update" - url = "jdbc:hsqldb:mem:testDb" - } - hibernate { - cache.use_second_level_cache = false - cache.use_query_cache = false - } - } - production { - dataSource { - dbCreate = "update" - url = "jdbc:hsqldb:file:${System.properties.'user.home'}/.frontlinesms2/prodDb;shutdown=true" - } - - } - - standalone { - dataSource { - dbCreate = "update" - url = "jdbc:hsqldb:file:standalone;shutdown=true" - } - } -} diff --git a/grails-app/conf/UrlMappings.groovy b/grails-app/conf/UrlMappings.groovy deleted file mode 100644 index aa8afc164..000000000 --- a/grails-app/conf/UrlMappings.groovy +++ /dev/null @@ -1,11 +0,0 @@ -class UrlMappings { - static mappings = { - "/"(controller:'message') - "/$controller/$action?/$id?"{ - constraints { - // apply constraints here - } - } - "500"(view:'/error') - } -} diff --git a/grails-app/conf/spring/resources.groovy b/grails-app/conf/spring/resources.groovy deleted file mode 100644 index fa950068b..000000000 --- a/grails-app/conf/spring/resources.groovy +++ /dev/null @@ -1,3 +0,0 @@ -// Place your Spring DSL code here -beans = { -} diff --git a/grails-app/views/_css.gsp b/grails-app/views/_css.gsp deleted file mode 100644 index 777777795..000000000 --- a/grails-app/views/_css.gsp +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - diff --git a/grails-app/views/archive/_header.gsp b/grails-app/views/archive/_header.gsp deleted file mode 100644 index 358114c87..000000000 --- a/grails-app/views/archive/_header.gsp +++ /dev/null @@ -1,17 +0,0 @@ -
-
- -

${messageSection} Archive

-
- -

${messageSection} Archive

-
- -

Activity Archive

-
- -

${messageSection} Archive

-
- -
-
\ No newline at end of file diff --git a/grails-app/views/error.gsp b/grails-app/views/error.gsp deleted file mode 100644 index 7fcd77dfc..000000000 --- a/grails-app/views/error.gsp +++ /dev/null @@ -1,26 +0,0 @@ - - - FrontlineSMS Exception - - - -

FrontlineSMS Exception

- - - Error ${request.'javax.servlet.error.status_code'}: ${request.'javax.servlet.error.message'.encodeAsHTML()} ${LINE_BREAK} - Servlet: ${request.'javax.servlet.error.servlet_name'} ${LINE_BREAK} - URI: ${request.'javax.servlet.error.request_uri'}${LINE_BREAK} - - Exception Message: ${exception.message?.encodeAsHTML()} ${LINE_BREAK} - Caused by: ${exception.cause?.message?.encodeAsHTML()} ${LINE_BREAK} - Class: ${exception.className} ${LINE_BREAK} - At Line: [${exception.lineNumber}] ${LINE_BREAK} - - - -
- - -
- - \ No newline at end of file diff --git a/images/grid.png b/images/grid.png deleted file mode 100644 index 42e2d69d5..000000000 Binary files a/images/grid.png and /dev/null differ diff --git a/lib/grails-snapshot-dependencies-fix-0.1.jar b/lib/grails-snapshot-dependencies-fix-0.1.jar deleted file mode 100644 index 0296ed236..000000000 Binary files a/lib/grails-snapshot-dependencies-fix-0.1.jar and /dev/null differ diff --git a/plugins/frontlinesms-core/.gitignore b/plugins/frontlinesms-core/.gitignore index 7d340ac0b..f91dced6d 100644 --- a/plugins/frontlinesms-core/.gitignore +++ b/plugins/frontlinesms-core/.gitignore @@ -3,3 +3,11 @@ web-app/WEB-INF/grails-app/ # Auto-generated JS i18n bundles. web-app/i18n/ + +# NodeJS local modules +node_modules/ + +# temp files when built as a plugin +web-app/css/frontlinesms-core +*.pom + diff --git a/plugins/frontlinesms-core/FrontlinesmsCoreGrailsPlugin.groovy b/plugins/frontlinesms-core/FrontlinesmsCoreGrailsPlugin.groovy index 60ec24221..13d584486 100644 --- a/plugins/frontlinesms-core/FrontlinesmsCoreGrailsPlugin.groovy +++ b/plugins/frontlinesms-core/FrontlinesmsCoreGrailsPlugin.groovy @@ -1,9 +1,9 @@ class FrontlinesmsCoreGrailsPlugin { - def version = "2.0-SNAPSHOT" + def version = '3.48-SNAPSHOT' def grailsVersion = "2.0.3" def pluginExcludes = ["grails-app/views/error.gsp", "grails-app/conf/CoreBootStrap.groovy", - "grails-app/conf/CoreUrlMappings.groovy"] + "grails-app/conf/frontlinesms2/SecurityFilters.groovy"] def author = "FrontlineSMS team" def authorEmail = "dev@frontlinesms.com" def title = "FrontlineSMS Core" diff --git a/plugins/frontlinesms-core/README.md b/plugins/frontlinesms-core/README.md index 7f222c95e..12a76d0b1 100644 --- a/plugins/frontlinesms-core/README.md +++ b/plugins/frontlinesms-core/README.md @@ -1,10 +1,11 @@ -Grails 2 Upgrade -================ +# FrontlineSMS v2 core codebase -# Testing +This folder contains +- the codebase for FrontlineSMS 2.x in the standard Grails application layout (grails-app, src, test, web-app, etc) +- Utility scripts, mainly in bash and groovy, in the 'do/' folder +- install4j & maven config for bundling the desktop version of FrontlineSMS in the 'install' folder -## Functional tests -Problems currently appear to be: -* override of `@href` in BootStrap not working - 'http://localhost:8080' is not stripped -* `$(...).jquery.*` not working - throws Exception -* database returns results in different orders now +Getting started +- Run `grails -Ddb.migrations=false run-app` to start the app in dev mode, with bootstrap data, and with database migrations disabled +- Run `grails test-app` to run our test suite, and `do/test_backup` to open the results in a browser +- Run `grails dependency-report` to view the list of plugin dependencies. If you need access to a FrontlineSMS-developed plugin that is not available in a public repo, please let us know via a github issue. diff --git a/plugins/frontlinesms-core/application.properties b/plugins/frontlinesms-core/application.properties index ec0d49169..b65b86352 100644 --- a/plugins/frontlinesms-core/application.properties +++ b/plugins/frontlinesms-core/application.properties @@ -1,6 +1,9 @@ #Grails Metadata file -#Thu Nov 15 15:41:15 EAT 2012 +#Wed May 21 09:55:52 EAT 2014 app.grails.version=2.0.3 app.name=frontlinesms-core app.servlet.version=2.5 -app.version=2.0-SNAPSHOT +app.version=5.0-SNAPSHOT +plugins.rest-client-builder=1.0.2 +plugins.spock=0.6 +sprint.number=78 diff --git a/plugins/frontlinesms-core/asdf/routes.dot b/plugins/frontlinesms-core/asdf/routes.dot new file mode 100644 index 000000000..ba9af78e1 --- /dev/null +++ b/plugins/frontlinesms-core/asdf/routes.dot @@ -0,0 +1,120 @@ +digraph { + handle_disconnect[label="fconnectionService.handleDisconnect()"] + smslib_disconnect[label="fconnectionService.handleDisconnection()"] + smslib_endpoint[label="SmslibEndpoint"] + smpp_endpoint[label="SmppEndpoint"] + http_endpoint[label="HTTPEndpoint"] + email_endpoint[label="EmailEndpoint"] + fmessage_storage[label="MessageStorageService.process()"] + subgraph cluster_outgoing { + label="Outgoing Messages" + a[label="MessageSendService.send()"] + c[label="seda:dispatches"] + router[label="DispatchRouterService.slip()"] + modem_out[label="seda:out-modem-${id}"] + internet_out[label="seda:out-internet-${id}"] + out_failuer[label="onFailureOnly"] + out_failure_handler[label="dispatchRouterService.handleFailed()"] + out_success[label="onSuccessOnly"] + out_success_handler[label="dispatchRouterService.handleCompleted()"] + + a -> fmessage_storage + fmessage_storage -> c + c -> router + router -> modem_out + router -> internet_out + out_failuer -> out_failure_handler + out_success -> out_success_handler + + subgraph cluster_smslib_out { + label="SMSLib (outgoing)" + modem_out + smslib_translate[label="SmslibTranslationService.toCMessage()"] + modem_out -> smslib_translate + smslib_translate -> smslib_endpoint + } + subgraph cluster_nexmo_oauth_out { + label="Nexmo OAuth (outgoing)" + nexmo_oauth_pre[label="OauthNexmoPreProcessor.process()"] + nexmo_oauth_post[label="OauthNexmoPostProcessor.process()"] + nexmo_oauth_endpoint[label="OauthNexmoEndpoint"] + error_handler[label="AuthenticationException\nInvalidApiIdException\nInsufficientCreditException"] + internet_out -> nexmo_oauth_pre + nexmo_oauth_pre -> nexmo_oauth_endpoint + nexmo_oauth_endpoint -> nexmo_oauth_post + error_handler -> handle_disconnect + } + subgraph cluster_smpp_out { + label="SMPP (outgoing)" + smpp_pre[label="SmppPreprocessor.process()"] + smpp_post[label="SmppPostprocessor.process()"] + internet_out -> smpp_pre + smpp_pre -> smpp_endpoint + smpp_endpoint -> smpp_post + } + } + subgraph cluster_incoming { + label="Incoming Messages" + in_store_queue[label="seda:incoming-fmessages-to-store"] + in_process_queue[label="seda:incoming-fmessages-to-process"] + in_router[label="IncomingMessageRouterService.route()"] + keyword_processor[label="KeywordProcessorService.process()"] + + in_store_queue -> fmessage_storage + fmessage_storage -> in_process_queue + in_process_queue -> in_router + in_router -> keyword_processor + + subgraph cluster_smslib_in { + label="SMSLib (incoming)" + smslib_in[label="seda:raw-smslib"] + smslib_in_exception[label="Exception"] + smslib_in_translate[label="SmslibTranslationService.toFmessage()"] + smslib_in_exception -> smslib_disconnect + smslib_endpoint -> smslib_in + smslib_in -> smslib_in_translate + smslib_in_translate -> in_store_queue + } + subgraph cluster_smpp_in { + label="SMPP (incoming)" + smpp_in[label="seda:raw-smpp"] + smpp_in_translate[label="SmppTranslationService.toFmessage()"] + smpp_endpoint -> smpp_in + smpp_in -> smpp_in_translate + smpp_in_translate -> in_store_queue + } + subgraph cluster_intellisms_in { + label="IntelliSMS (incoming)" + intellisms_in_translate[label="IntellismsTranslationService.process()"] + email_endpoint -> intellisms_in_translate + intellisms_in_translate -> in_store_queue + } + + subgraph cluster_radio_in { + label="Radio" + radio_in_queue[label="seda:radioshow-fmessages-to-process"] + in_router -> radio_in_queue + } + + subgraph cluster_activity_processors { + label="Activities" + activity_processor[label="Activity.process()"] + + keyword_processor -> activity_processor + + subgraph cluster_webconnection_processor { + label="Webconnections" + + wc_queue[label="seda:activity-webconnection-${id}"] + wc_pre[label="WebconnectionService.preProcess()"] + wc_post[label="WebconnectionService.postProcess()"] + + activity_processor -> wc_queue + wc_queue -> wc_pre + wc_pre -> http_endpoint + http_endpoint -> wc_post + } + } + } +} + diff --git a/plugins/frontlinesms-core/bails b/plugins/frontlinesms-core/bails new file mode 120000 index 000000000..1892127ae --- /dev/null +++ b/plugins/frontlinesms-core/bails @@ -0,0 +1 @@ +target/plugins/bails-0.6/scripts \ No newline at end of file diff --git a/plugins/frontlinesms-core/do/add_current_user_to_dialout_group b/plugins/frontlinesms-core/do/add_current_user_to_dialout_group new file mode 100755 index 000000000..20d50a569 --- /dev/null +++ b/plugins/frontlinesms-core/do/add_current_user_to_dialout_group @@ -0,0 +1,8 @@ +#!/bin/bash +set -e +U=`id -nu` +G=dialout +echo "# Adding user '$U' to group '$G'..." +sudo usermod -a -G $G $U +echo "# Added." + diff --git a/plugins/frontlinesms-core/do/add_launcher_without_splash b/plugins/frontlinesms-core/do/add_launcher_without_splash new file mode 100755 index 000000000..7acdbd5fb --- /dev/null +++ b/plugins/frontlinesms-core/do/add_launcher_without_splash @@ -0,0 +1,20 @@ +#!/usr/bin/env groovy +import javax.xml.parsers.DocumentBuilderFactory +def buildXml = new File('install', 'build.install4j') +println buildXml.absolutePath +def builder = DocumentBuilderFactory.newInstance().newDocumentBuilder() +def conf = builder.parse(buildXml.newInputStream()).documentElement +def child = { node, childName -> node.childNodes.find { it.nodeName == childName } } + +def launcherXml = ['launchers', 'launcher'].inject(conf, child) + +def noSplashLauncher = launcherXml.cloneNode(true) +child(noSplashLauncher, 'splashScreen').setAttribute('show', 'false') +def exeNode = child(noSplashLauncher, 'executable') +exeNode.setAttribute('name', exeNode.getAttribute('name') + '_no_splash') + +child(conf, 'launchers').appendChild(noSplashLauncher) +child(conf, 'launchers').childNodes*.nodeName + +buildXml.text = conf as String + diff --git a/plugins/frontlinesms-core/do/build_deploy_plugin b/plugins/frontlinesms-core/do/build_deploy_plugin new file mode 100755 index 000000000..ff695c608 --- /dev/null +++ b/plugins/frontlinesms-core/do/build_deploy_plugin @@ -0,0 +1,125 @@ +#!/bin/bash +set -e + +# Extract flags +binaryFlag="" +buildBinary=false +offlineFlag="" +while [[ $1 == "--"* ]]; do + if [ "$1" == "--m2-deploy" ]; then + echo "# Maven deployment enabled." + m2deploy=true + fi + if [[ "$1" == "--binary" ]]; then + echo "# Building plugin as binary." + binaryFlag="--binary" + buildBinary=true + fi + if [[ "$1" == "--offline" ]]; then + echo "# Building plugin offline" + offlineFlag="--offline" + fi + shift +done + +echo "# Extracting grails version from application.properties..." +grailsVersion=$(grep 'app\.grails\.version=' application.properties | cut -d= -f2) +echo "# Grails version is $grailsVersion" + +echo "# Extracting app name from application.properties..." +pluginName=`ls *GrailsPlugin.groovy | cut -d. -f1 | sed -E -e 's/^(.*)GrailsPlugin$/\1/' | sed -E 's/(([^^])([A-Z]))/\2-\3/g' | tr [:upper:] [:lower:]` +echo "# Extracted plugin name as $pluginName" + +echo "# Extracting version from application.properties..." +set +e; grep -q 'version.*binary' *GrailsPlugin.groovy +binaryVersionSet=$? +set -e +if $buildBinary; then + if [[ "$binaryVersionSet" == "1" ]]; then + echo "# Changing plugin version to include binary and grails version in the name." + sed -E -i -e "s/^\s*def\s+version\s*=\s*['\"](.*?)(-SNAPSHOT)?['\"]\s*$/\tdef version = \"\1-binary-$grailsVersion\2\"/g" *GrailsPlugin.groovy + elif [[ "$binaryVersionSet" == "0" ]]; then + echo "# Version is already set for binary." + else + echo "# Error checking if binary version set." + exit 1 + fi +else + if [[ "$binaryVersionSet" == "0" ]]; then + echo "# Removing binary info from plugin version..." + sed -E -i -e "s/^\s*def\s+version\s*=\s*['\"](.*?)(-binary-$grailsVersion)(-SNAPSHOT)?['\"]\s*$/\tdef version = '\1\3'/" *GrailsPlugin.groovy + elif [[ "$binaryVersionSet" != "1" ]]; then + echo "# Error checking if binary version set." + exit 1 + fi +fi +pluginVersion=$(env groovy -e "`grep -E '^\s*def\s+version' *GrailsPlugin.groovy`; println version") +echo "# Extracted plugin version as $pluginVersion" +rm plugin.xml || echo "# no plugin.xml present" +grails --non-interactive clean +grails --non-interactive --stacktrace $binaryFlag $offlineFlag package-plugin + +if $buildBinary; then + oldPackageName=target/grails-plugin-$pluginName-$pluginVersion.jar + newPackageName=$pluginName-$pluginVersion.jar +else + newPackageName=$pluginName-$pluginVersion.zip + oldPackageName=grails-$newPackageName +fi + +echo "# renaming $oldPackageName to $newPackageName" +mv $oldPackageName $newPackageName + +if $buildBinary; then + echo "..." +else + ivyCacheDir=~/.grails/ivy-cache/org.grails.plugins/$pluginName/zips + mkdir -p $ivyCacheDir + ivyCachedZip=$ivyCacheDir/$newPackageName + echo "# Removing old plugin from local ivy cache at $ivyCachedZip..." + rm $ivyCachedZip || echo "# Old plugin was not found in ivy cache." + echo "# Deploying to local ivy cache..." + cp $newPackageName $ivyCachedZip + echo "# Plugin built and deployed to local ivy cache." + +fi +mavenFileName="$newPackageName" + +echo "# Deploying to local maven repository..." +mavenRepoDir=~/.m2/repository/org/grails/plugins/$pluginName/$pluginVersion/ +echo "# Creating maven repo dir at $mavenRepoDir..." +mkdir -p $mavenRepoDir +echo "# Deploying to ${mavenRepoDir}$mavenFileName" +cp $mavenFileName $mavenRepoDir +echo "# Deployed to local maven repository." + +echo "# Building pom file..." +grails generate-pom $binaryFlag +echo "# Deploying POM file to local maven repository..." +pomFileName="$pluginName-$pluginVersion.pom" +cp target/pom.xml $pomFileName +echo "# Deploying to ${mavenRepoDir}$pomFileName" +cp $pomFileName ${mavenRepoDir}$pomFileName +echo "# POM deployed" + +if [ -n "$m2deploy" ]; then + echo "# Deploy core plugin to remote plugin repo..." + echo "# Using maven password: $FRONTLINESMS_MAVEN_PASSWORD" + ftp -nv dev.frontlinesms.com << EOF +hash +user m2repo $FRONTLINESMS_MAVEN_PASSWORD +mkdir org +mkdir org/grails/plugins +mkdir org/grails/plugins/$pluginName +cd org/grails/plugins/$pluginName +mkdir $pluginVersion +cd $pluginVersion +put $mavenFileName +put $pomFileName +exit +EOF + echo "# Plugin deployed to remote maven repo." +fi + +echo "# Deployment complete." + diff --git a/plugins/frontlinesms-core/do/build_installers_with_migrations_disabled b/plugins/frontlinesms-core/do/build_installers_with_migrations_disabled index abf125cb7..6a1b03951 100755 --- a/plugins/frontlinesms-core/do/build_installers_with_migrations_disabled +++ b/plugins/frontlinesms-core/do/build_installers_with_migrations_disabled @@ -8,6 +8,5 @@ fi echo "# running grails clean..." grails clean echo "# build installers with migrations disabled for grails env $GRAILS_ENV..." -grails -Dfrontlinesms2.build.db.migrations=false $GRAILS_ENV build-installers -echo "# installers built. Have a nice day." +grails -Dfrontlinesms2.build.db.migrations=false $GRAILS_ENV build-installers && echo "# installers built. Have a nice day." diff --git a/plugins/frontlinesms-core/do/build_uninstall_install_test b/plugins/frontlinesms-core/do/build_uninstall_install_test index bcf6153b1..f17a5d26d 100755 --- a/plugins/frontlinesms-core/do/build_uninstall_install_test +++ b/plugins/frontlinesms-core/do/build_uninstall_install_test @@ -11,15 +11,9 @@ echo "## 5. check that the server is working! ##" echo "#############################################" PROJECT_DIR=../frontlinesms-core -INSTALL_DIR=~/frontlinesms2 -ALL_USER_INSTALL_DIR=/opt/frontlinesms2 -SETTINGS_DIR=~/.frontlinesms2 -SERVER_PORT=8130 -CONTEXT_PATH= echo "# clean target directories" rm -rf $PROJECT_DIR/target/ -rm -rf target/ rm -rf $PROJECT_DIR/install/target* rm -rf $PROJECT_DIR/install/src/web-app/ @@ -27,54 +21,5 @@ echo "# build-installers" grails clean grails -Dfrontlinesms2.build.confirmNotProd=true build-installers $@ -echo "# uninstall old version" -$INSTALL_DIR/uninstall -q || echo "# Could not run uninstaller in $INSTALL_DIR" -$ALL_USER_INSTALL_DIR/uninstall -q || echo "# Could not run uninstaller in $ALL_USER_INSTALL_DIR" - -echo "# remove old install directory" -rm -rf $INSTALL_DIR/ || rm -rf $ALL_USER_INSTALL_DIR || echo "# Could not remove installation directory as it did not exist" - -echo "# remove old settings/database directory" -rm -rf $SETTINGS_DIR/ - -echo "# install new build" -$PROJECT_DIR/install/target/install4j/*.sh -q || echo "There was an error running the installer TODO supply a valid email address on commandline so this does not fail." - -echo "# run new install in background" -$INSTALL_DIR/*_Launcher || $ALL_USER_INSTALL_DIR/*_Launcher & -LAUNCHER_PROCESS_ID=`jobs -p` -LAUNCHER_PROCESS_GROUP_ID=`ps -j --pid $LAUNCHER_PROCESS_ID | tail -n1 | grep -Po -m2 "\w+" | grep -Pm2 "\w+" | tail -n1` -jobs -echo "Launched process ID: $LAUNCHER_PROCESS_ID with group $LAUNCHER_PROCESS_GROUP_ID" - -RESPONSE="000" -PING_URL=http://localhost:$SERVER_PORT$CONTEXT_PATH/status/show -echo "# Waiting for server to start" -echo "# Ping URL: $PING_URL" -until [ "$RESPONSE" -ne "000" ]; do - echo "# Pinging $PING_URL..." - RESPONSE=`curl -o /dev/null --silent --head --write-out '%{http_code}' $PING_URL` || echo "Setting response seems to give an error code" - echo "# Pinged server at $PING_URL and got response: $RESPONSE" - sleep 10 -done - -echo "# Final server response: $RESPONSE" -if [ "$RESPONSE" -eq "200" ]; then - echo "# Started FrontlineSMS successfully \\o/" - EXIT_CODE=0 -else - echo "# Error starting FrontlineSMS" - EXIT_CORE=1 -fi - -echo "# Killing FrontlineSMS instance" -CHILD_PROCESSES=`ps h -o pid -$LAUNCHER_PROCESS_GROUP_ID | tac | tail -n +2` -echo "# Detected processes: $CHILD_PROCESSES" -echo "# Excluding process: $LAUNCHER_PROCESS_GROUP_ID" -CHILD_PROCESSES=`echo "$CHILD_PROCESSES" | sed s/$LAUNCHER_PROCESS_GROUP_ID//` -echo "# Killing child processes: $CHILD_PROCESSES" -kill -TERM $CHILD_PROCESSES || echo "Kill doesn't give us a nice exit code BTW" - -echo "Exiting with code: $EXIT_CODE" -exit $EXIT_CODE +do/uninstall_install_test diff --git a/plugins/frontlinesms-core/do/check_for_bad_resource_tags b/plugins/frontlinesms-core/do/check_for_bad_resource_tags new file mode 100755 index 000000000..4ebb625d1 --- /dev/null +++ b/plugins/frontlinesms-core/do/check_for_bad_resource_tags @@ -0,0 +1,10 @@ +#!/bin/bash +echo "# Checking for calls to g.resource instead of r.resource..." +if grep -Er '(g:|g\.|\{|<)resource' grails-app/views/; then + echo "# Bad calls to g.resource found. Please change these to use r.resource." + exit 1 +else + echo "# No bad calls to g.resource found." + exit 0 +fi + diff --git a/plugins/frontlinesms-core/do/check_for_bad_template_renders b/plugins/frontlinesms-core/do/check_for_bad_template_renders new file mode 100755 index 000000000..f83749a80 --- /dev/null +++ b/plugins/frontlinesms-core/do/check_for_bad_template_renders @@ -0,0 +1,14 @@ +#!/bin/bash +echo "# Checking for bad calls to template renderer..." +echo "# Checking views..." +grep -rn "render.*template" grails-app/views/ | grep -v "fsms:render" +VIEW_OUTPUT=$? + +if [ "$VIEW_OUTPUT" -eq 1 ]; then + echo "# No bad template renders detected" + exit 0 +else + echo "# Bad template renders detected" + exit 1 +fi + diff --git a/plugins/frontlinesms-core/do/check_for_camelprocessors_using_def b/plugins/frontlinesms-core/do/check_for_camelprocessors_using_def new file mode 100755 index 000000000..4e8e8c5b5 --- /dev/null +++ b/plugins/frontlinesms-core/do/check_for_camelprocessors_using_def @@ -0,0 +1,17 @@ +#!/bin/bash +echo "# Checking for any CamelProcessors using 'def' instead of 'void'..." +grep "def *process" -r src/groovy/frontlinesms2/camel/ +result="$?" + +if [[ "$result" == "1" ]]; then + echo "# No CamelProcessor using 'def' found." + exit 0 +fi + +if [[ "$result" == "0" ]]; then + echo "# CamelProcessors using 'def' found." + exit 1 +fi + +echo "# There was a problem checking for camel processors using 'def' instead of 'void'." +exit $? \ No newline at end of file diff --git a/plugins/frontlinesms-core/do/check_for_ignored_tests b/plugins/frontlinesms-core/do/check_for_ignored_tests index 7b6759f47..d9363a0bf 100755 --- a/plugins/frontlinesms-core/do/check_for_ignored_tests +++ b/plugins/frontlinesms-core/do/check_for_ignored_tests @@ -1,5 +1,15 @@ #!/bin/bash -echo "Running 'grep -irl spock.lang.ignore test'" -echo "Matching files::" -grep -irl spock.lang.ignore test +echo "# Searching for ignored tests..." +grep -Er '@(spock\.lang\.)?Ignore' test/ +FOUND_STATUS=$? + +if [ 1 -eq $FOUND_STATUS ]; then + echo "# No ignored tests were found." + RETURN_STATUS=0 +else + echo "# Ignored tests were found, or there was a problem with the search." + RETURN_STATUS=1 +fi + +exit $RETURN_STATUS diff --git a/plugins/frontlinesms-core/do/check_for_metaclass_in_non-unit_tests b/plugins/frontlinesms-core/do/check_for_metaclass_in_non-unit_tests new file mode 100755 index 000000000..9c4a67d5f --- /dev/null +++ b/plugins/frontlinesms-core/do/check_for_metaclass_in_non-unit_tests @@ -0,0 +1,10 @@ +#!/bin/bash +echo "# Checking for calls to metaclass on non-instance variables..." +grep -r '\s[A-Z]([a-zA-Z]*)\.metaClass' test/integration/ test/functional/ +if [ $? -eq 1 ]; then + echo "# No bad metaclass calls found." + exit 0 +fi +echo "# Bad Class.metaClass calls detected." +exit 1 + diff --git a/plugins/frontlinesms-core/do/check_for_missing_fa_class b/plugins/frontlinesms-core/do/check_for_missing_fa_class new file mode 100755 index 000000000..2bf0fb09a --- /dev/null +++ b/plugins/frontlinesms-core/do/check_for_missing_fa_class @@ -0,0 +1,15 @@ +#!/bin/bash +echo "# Searching for fontawesome icons without fa class..." +grep -Er 'fa-' grails-app/views/ | grep -v 'fa ' +FOUND_STATUS=$? + +if [ 1 -eq $FOUND_STATUS ]; then + echo "# No incorrect font awesome icon usage was found." + RETURN_STATUS=0 +else + echo "# Font Awesome icons without the fa class were found. These work on Chromium for linux but not other browsers. Please add 'fa' class" + RETURN_STATUS=1 +fi + +exit $RETURN_STATUS + diff --git a/plugins/frontlinesms-core/do/check_for_popups_without_loading_image b/plugins/frontlinesms-core/do/check_for_popups_without_loading_image new file mode 100755 index 000000000..b0a4b3dd9 --- /dev/null +++ b/plugins/frontlinesms-core/do/check_for_popups_without_loading_image @@ -0,0 +1,12 @@ +#!/bin/bash + +echo "# Checking for popups that dont have loading image..." +grep -Er 'launch.*Popup|launch.*Wizard' grails-app/views/ | grep onSuccess +if [[ "$?" == "1" ]]; then + echo "# No popups without loading image detected." + exit 0 +else + echo "# VIOLATION: Popups without loading image detected, or there was a problem with the search." + exit 1 +fi + diff --git a/plugins/frontlinesms-core/do/check_for_setInterval_calls b/plugins/frontlinesms-core/do/check_for_setInterval_calls new file mode 100755 index 000000000..8a7aca5f5 --- /dev/null +++ b/plugins/frontlinesms-core/do/check_for_setInterval_calls @@ -0,0 +1,14 @@ +#!/bin/bash +set -e + +echo "# Checking for bad calls to setInterval..." +VIOLATIONS=`grep -lr setInterval grails-app/views/ web-app/js/ | grep -v app_info | grep -v timer | wc -l | tr -d ' '` + +if [[ "$VIOLATIONS" != "0" ]]; then + echo "# Violations found:" + grep -lr setInterval grails-app/views/ web-app/js/ | grep -v app_info | grep -v timer + exit 1 +fi + +echo "# No bad calls to setInterval found." + diff --git a/plugins/frontlinesms-core/do/check_for_static_simple_date_format b/plugins/frontlinesms-core/do/check_for_static_simple_date_format new file mode 100755 index 000000000..9b5ab0955 --- /dev/null +++ b/plugins/frontlinesms-core/do/check_for_static_simple_date_format @@ -0,0 +1,10 @@ +#!/bin/bash +echo "# Checking for non-thread-safe use of SimpleDateFormat..." +grep -r 'static.*SimpleDateFormat' grails-app/ src/ +if [[ "$?" != "1" ]]; then + echo "# Static instances of SimpleDateFormat found. This class is not thread-safe." + echo "# Either make the instances ThreadLocal or per-instance." + exit 1 +fi +echo "# No bad SimpleDateFormat use found." + diff --git a/plugins/frontlinesms-core/do/clean_naughty_camel b/plugins/frontlinesms-core/do/clean_naughty_camel new file mode 100755 index 000000000..b55532c9d --- /dev/null +++ b/plugins/frontlinesms-core/do/clean_naughty_camel @@ -0,0 +1,16 @@ +#!/bin/bash +set -e + +function clean_naughty_camel { + CAMEL_DIR="$1" + echo "# Removing $CAMEL_DIR..." + rm -rf $CAMEL_DIR + echo "# Camel dir removed." +} + +clean_naughty_camel target/plugins/routing-1.2.2-camel-2.9.4/ + +APP_NAME=`grep '^app.name=' application.properties | cut -d= -f2` +GRAILS_VERSION=`grep '^app.grails.version=' application.properties | cut -d= -f2` +clean_naughty_camel ~/.grails/$GRAILS_VERSION/projects/$APP_NAME/plugins/routing-1.2.2-camel-2.9.4/ + diff --git a/plugins/frontlinesms-core/do/create_sprint_build b/plugins/frontlinesms-core/do/create_sprint_build new file mode 100755 index 000000000..abe502186 --- /dev/null +++ b/plugins/frontlinesms-core/do/create_sprint_build @@ -0,0 +1,41 @@ +#!/bin/bash +set -e + +# Abort if git repo is not clean +if test -n "$(git status --porcelain)" +then + echo "ABORT The sprint build has been terminated due to unclean git repo" + exit 1 +fi + +# Read sprint number from application.properties +sprintNumber=`sed '/^\#/d' ./application.properties | grep "sprint.number" | cut -d "=" -f 2` +appVersion=`sed '/^\#/d' ./application.properties | grep "app.version" | cut -d "=" -f 2` + +# Remove and back up application version +sed 's/app\.version=/app\.version\.backup=/g' -i application.properties +echo "app.version=SPRINT-$sprintNumber" >> application.properties + +git add application.properties +git commit -m "SPRINT-BUILD-$sprintNumber SETUP: Backup app.version [$appVersion] and change to SPRINT-$sprintNumber for BuildInstaller script" + +echo "Generating build for iteration $sprintNumber" + +# Generate installers with migrations disabled +grails -Ddb.migrations=false prod build-installers --stacktrace + +# Tag build +git tag "SPRINT-$sprintNumber" + +# Restore app.version +git checkout application.properties # This removes the cruft that gets added every time you compile the app +sed '/app\.version=/d' -i application.properties +sed 's/app\.version\.backup=/app\.version=/g' -i application.properties +git commit -m "SPRINT-BUILD-$sprintNumber CLEANUP: Restore app.version to $appVersion" + +# Bumping up sprint number +sed "s/sprint\.number=`echo $sprintNumber`/sprint\.number=`expr $sprintNumber + 1`/g" -i application.properties +git add application.properties +git commit -m "SPRINT-BUILD-$sprintNumber POST-BUILD: Bumping up sprint.number to `expr $sprintNumber + 1`" + +# Push installers to s3 bucket, "sprint-builds" diff --git a/plugins/frontlinesms-core/do/deploy_builds b/plugins/frontlinesms-core/do/deploy_builds new file mode 100755 index 000000000..2b7b4690a --- /dev/null +++ b/plugins/frontlinesms-core/do/deploy_builds @@ -0,0 +1,90 @@ +#!/bin/bash +if [ ! -z "$1" ] && [ ! -z "$2" ] +then + echo "AWESOME #Username : $1 #Password : $2" +else + echo "Upload files to your server" + echo "Error ### Usage: ./do/deploy_builds " + exit +fi +USERNAME=$1 +PASSWORD=$2 + +root_url="ftp://frontlinesms.com/" +remote_upload_path=$root_url"/httpdocs/downloads/" + +local_windows_path=`ls install/target/install4j/frontlinesms* | grep window` +local_mac_path=`ls install/target/install4j/frontlinesms* | grep mac` +local_unix_path=`ls install/target/install4j/frontlinesms* | grep unix` + +r_path="http://www.frontlinesms.com/downloads/" + +windows_file=`ls install/target/install4j/ | grep windows` +mac_file=`ls install/target/install4j/ | grep mac` +unix_file=`ls install/target/install4j/ | grep unix` + +r_windows_path=$r_path$windows_file +r_mac_path=$r_path$mac_file +r_unix_path=$r_path$unix_file + +echo ">>>>>>>>>>> Uploading to"$remote_upload_path +#uloading the builds +echo "#### Uloading windows build.." +curl -v -T $local_windows_path -u $USERNAME:$PASSWORD -Q "TYPE I" "$remote_upload_path" +echo "#### Uloading mac build.." +curl -v -T $local_mac_path -u $USERNAME:$PASSWORD -Q "TYPE I" "$remote_upload_path" +echo "#### Uploading unix build..." +curl -v -T $local_unix_path -u $USERNAME:$PASSWORD -Q "TYPE I" "$remote_upload_path" + +#Cheking md5 of files +echo "Checking md5 of Windows build" +local_hash=$(md5sum `ls install/target/install4j/frontlinesms* | grep windows`) +declare -a final_local_hash="(${local_hash[0]})"; +declare remote_hash=`curl http://frontlinesms.com/downloads/checkmd5.php?filename=$windows_file` + +echo "Local md5sum : $final_local_hash ## Remote md5sum : $remote_hash" +if [ $final_local_hash = $remote_hash ]; then + echo "md5sum okay ## oh yeah !!! "; +else + echo "ooops md5sum not equal exiting script" + exit 0 +fi + +echo "Checking md5 of Mac build" +local_hash=$(md5sum `ls install/target/install4j/frontlinesms* | grep mac`) +declare -a final_local_hash="(${local_hash[0]})"; +declare remote_hash=`curl http://frontlinesms.com/downloads/checkmd5.php?filename=$mac_file` + +echo "Local md5sum : $final_local_hash ## Remote md5sum : $remote_hash" +if [ $final_local_hash = $remote_hash ]; then + echo "md5sum okay ## oh yeah !!! "; +else + echo "ooops md5sum not equal exiting script" + exit 0 +fi + +local_hash=$(md5sum `ls install/target/install4j/frontlinesms* | grep unix`) +declare -a final_local_hash="(${local_hash[0]})"; +declare remote_hash=`curl http://frontlinesms.com/downloads/checkmd5.php?filename=$unix_file` + +echo "Local md5sum : $final_local_hash ## Remote md5sum : $remote_hash" +if [ $final_local_hash = $remote_hash ]; then + echo "md5sum okay ## oh yeah !!! " +else + echo "ooops md5sum not equal exiting script" + exit 0 +fi + + + +echo "#### Creating the xml file with file names" +touch build_links.php +echo "" >> build_links.php +echo "#### Uloading xml document with file names" +local_json_path="build_links.php" +curl -v -T $local_json_path -u $USERNAME:$PASSWORD -Q "TYPE I" "$remote_upload_path" +cat build_links.php +echo "#### Removing build_links.php... :(" +rm build_links.php + +echo "Complete .... :)" diff --git a/plugins/frontlinesms-core/do/deploy_builds_s3 b/plugins/frontlinesms-core/do/deploy_builds_s3 new file mode 100755 index 000000000..6fc865a07 --- /dev/null +++ b/plugins/frontlinesms-core/do/deploy_builds_s3 @@ -0,0 +1,87 @@ +#!/bin/bash +# S3 Deployment script for FrontlineSMS. Requires $FRONTLINESMS_SSH_USER +# and # $FRONTLINESMS_SSH_PASS to be set and valid. +set -e +# set the subdomain where deoply will be done +subdomain=$1 + +#check if $FRONTLINESMS_SSH_USER and $FRONTLINESMS_SSH_PASS env variables are set +if [ -z "$FRONTLINESMS_SSH_USER" ] && [ -z "$FRONTLINESMS_SSH_PASS" ] ; then + echo "[error] FRONTLINESMS_SSH_USER or FRONTLINESMS_SSH_PASS has not been set" + exit 1 +fi +# get local filename/path +fileName() { + echo "$(ls install/target/install4j/ | grep $1)" +} +localPath() { + echo "$(ls install/target/install4j/frontlinesms* | grep $1)" +} + +# upload files to S3 bucket & verify afterwards +s3Upload() { + echo "Uploading to S3 bucket" + s3cmd put --acl-public --guess-mime-type $1 s3://${bucketName}/$2 + s3cmd get s3://${bucketName}/$2 ${tempDir}/$1 + + md5Loc=$(md5sum $1 | awk '{print $1}') + md5Rem=$(md5sum ${tempDir}/$1 | awk '{print $1}') + echo "Local md5 checksum: $md5Loc" + echo "Remote md5 checksum: $md5Rem" + + if [ "$md5Loc" != "$md5Rem" ] ; then + echo "... verification failed, exiting script" + exit 1 + else + echo "... verification successful" + fi +} + +# 0. vars & set up +bucketName="download-frontlinesms" +serverUrl="ftp://frontlinesms.com/" +if [ $subdomain == "www" ] ; then + serverPath="www/dl/" +else + serverPath="subdomains/wip/httpdocs/dl/" +fi +echo "htaccess file will be pushed to $serverUrl$serverPath" + +tempDir=".tmp-$RANDOM" +echo "Creating temporary directory at $tempDir" +mkdir $tempDir +echo "Setting up S3 access tokens" +do/setUpS3 + +touch ${tempDir}/.htaccess + +for osArch in "windows" "mac" "unix"; do + echo "Processing $osArch build..." + # 1. get paths & file names + osArchFilePath=$(localPath $osArch) + osArchFile=$(fileName $osArch) + + echo "Uploading $osArchFile" + # 2. upload and verify + s3Upload $osArchFilePath $osArchFile + + # 3. rewrite .htaccess + s3Link="http://$bucketName.s3.amazonaws.com/${osArchFile}" + echo "Creating redirect statement in .htaccess to $s3Link" + + echo "Redirect /dl/latest/$osArch $s3Link" >> ${tempDir}/.htaccess +done + +echo "Rewrote .htaccess with the following contents:" +cat ${tempDir}/.htaccess + +# 4. publish .htaccess +echo "Pushing .htaccess file to $serverUrl$serverPath" +curl -T "${tempDir}/.htaccess" -u $FRONTLINESMS_SSH_USER:\ +$FRONTLINESMS_SSH_PASS -Q "TYPE I" "$serverUrl$serverPath" + +echo "Cleaning up $tempDir" +# 5. cleanup +rm -R $tempDir + +echo "Complete" diff --git a/plugins/frontlinesms-core/do/functional_test_rot_checker b/plugins/frontlinesms-core/do/functional_test_rot_checker deleted file mode 100755 index 0d930a71b..000000000 --- a/plugins/frontlinesms-core/do/functional_test_rot_checker +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -if [ "$1" == "-v" ]; then - echo "Should set first" - VERBOSE=true - shift -fi -VIOLATIONS=0 - -echo "# All domain saves should be flushed in functional tests" -BAD_SAVE_COUNT=`grep -r "save(.*)" test/functional/ | grep -v "flush:\s*true" | grep -c .` -if [ "0" -ne "$BAD_SAVE_COUNT" ]; then - echo "# Found saves without flush: $BAD_SAVE_COUNT" - let ++VIOLATIONS - if [ -n "$VERBOSE" ]; then - grep -r "save(.*)" test/functional/ | grep -v "flush:\s*true" - fi -else - echo "# No bad saves found" -fi - -if [ $VIOLATIONS -gt "0" ]; then - echo "# Violations: $VIOLATIONS" - exit 1 -fi - -echo "# There were no violations" diff --git a/plugins/frontlinesms-core/do/functional_test_unflushed_saves_checker b/plugins/frontlinesms-core/do/functional_test_unflushed_saves_checker new file mode 100755 index 000000000..e17c2e03c --- /dev/null +++ b/plugins/frontlinesms-core/do/functional_test_unflushed_saves_checker @@ -0,0 +1,16 @@ +#!/bin/bash + +echo "# All domain saves should be flushed in functional tests" +grep -r "save(.*)" test/functional/ | grep -Ev "flush:\s*true|no-flush-deliberate" +searchResult=$? +if [[ "$searchResult" == "0" ]]; then + echo "# Found saves without flush." + exit 1 +elif [[ "$searchResult" == "1" ]]; then + echo "# No bad saves found." + exit 0 +fi + +echo "# Error with search. Terminating script." +exit 1 + diff --git a/plugins/frontlinesms-core/do/functional_test_unsafe_text_calls_checker b/plugins/frontlinesms-core/do/functional_test_unsafe_text_calls_checker index f0a645837..b469af5ea 100755 --- a/plugins/frontlinesms-core/do/functional_test_unsafe_text_calls_checker +++ b/plugins/frontlinesms-core/do/functional_test_unsafe_text_calls_checker @@ -1,10 +1,10 @@ #!/bin/bash -set -e -echo "# Checking for Unsafe null dereference calls to .text() method e.g. text().toLowerCase()" -BAD_TEXT_CALLS=`grep -rl ".text()\." test/functional/` -if [ -n "$BAD_TEXT_CALLS" ]; then - echo "Found unsafe calls to .text() in the following tests:" - grep -rl ".text()\." test/functional/ +echo "# Checking for Unsafe null dereference calls to .text() method e.g. text().toLowerCase()." +echo "# Ignoring spread operators, e.g. x*.text()" +grep -r "[^*]\.text()\." test/functional/ + +if [ 0 -eq $? ]; then + echo "Found unsafe calls to .text()." exit 1 fi diff --git a/plugins/frontlinesms-core/do/help/build b/plugins/frontlinesms-core/do/help/build new file mode 100755 index 000000000..c11e72f97 --- /dev/null +++ b/plugins/frontlinesms-core/do/help/build @@ -0,0 +1,70 @@ +#!/bin/bash + +generateZip=false +gitTree=false +showHelp=false +sshURL="frontlin@frontlinesms.com" +pushToServer=false + +OPTIND=1 + +while getopts "zt:ps:h" opt; do + case $opt in + t) gitTree=$OPTARG + ;; + z) generateZip=$OPTARG + ;; + h) showHelp=$OPTARG + ;; + p) pushToServer=true + ;; + s) sshURL=$OPTARG + ;; + esac +done + +if $showHelp; then + echo "Ahoy! I am the FrontlineSMS Help File Build script" + echo "usage: do/help/build [options]" + echo " -h: Show this help text and exit" + echo " -t : The git tree or index to checkout" + echo " -z: Set to generate a zip archive of the help files" + echo " -p: Push to help.frontlinesms.com" + echo " -s : SSH URL to push to. $sshURL by default" + exit 0 +fi + +tempDir="`mktemp -d`" +cp -r grails-app/conf/help/* $tempDir +cp -r web-app/* $tempDir +for f in $(find $tempDir -name \*.txt); do + filename=$(basename "$f") + filenameWithoutExtension="${filename%.*}" + pandoc --from=markdown --to=html $f -o $(dirname ${f})/$filenameWithoutExtension + rm $f +done + +for f in $(ls web-app | grep images --invert-match); do + rm -rf "$tempDir/$f" +done + +for f in $(ls web-app/images | grep help --invert-match); do + rm -rf "$tempDir/images/$f" +done + +mkdir -p target/generated_help; cp -r $tempDir/* $_ + +if $generateZip; then + echo "Generating ZIP archive" + zip target/help.zip -r $tempDir + echo "ZIP saved at target/help.zip" +fi + +if $pushToServer; then + scp -rp ./target/generated_help $sshURL:subdomains/help/httpdocs/manuals/generated_help +fi + +google-chrome target/generated_help + +exit 0 + diff --git a/plugins/frontlinesms-core/do/i18n-compare.groovy b/plugins/frontlinesms-core/do/i18n-compare.groovy deleted file mode 100644 index 2cb64a4e7..000000000 --- a/plugins/frontlinesms-core/do/i18n-compare.groovy +++ /dev/null @@ -1,43 +0,0 @@ -/* Script to compare grails internationalisation property files (or any property files in general) - * -*/ -if(args.size() < 2) -{ - println("compares i18n file to master message.properties") - println("usage: i18n-compare MASTER OTHER [APPLY-CHANGES] [NEW-FILE]") - println(" MASTER: The main property file, assumed to be correct") - println(" OTHER : The file to be compared to MASTER") - return -} -println "Reporting on differences betweer ${args[0]} and ${args[1]}.." - -// read files into props -master = new Properties() -new File(args[0]).withInputStream { stream -> master.load(stream); } -other = new Properties() -new File(args[1]).withInputStream { stream -> other.load(stream); } - -// init lists -def missingKeys = [] -def redundantKeys = [] - -master.keySet().each { - if(!other.get(it) || other.get(it).length() == 0) - { - missingKeys << it - } -} -redundantKeys = other.keySet() - master.keySet() - -redundantKeys.each { - other.remove(it) -} -missingKeys.each { - other.put(it, "TODO:"+master.get(it)) -} - -langName = other.get("language.name") - -double perc = ((master.size() - missingKeys.size()) / master.size()) * 100 -println("Redundant entries: ${redundantKeys.size()}, MissingEntries: ${missingKeys.size()}") -println "${langName? langName + ' translation' : args[1]} is ${perc.round(2)}% complete" \ No newline at end of file diff --git a/plugins/frontlinesms-core/do/i18n/check_for_bad_date_formats b/plugins/frontlinesms-core/do/i18n/check_for_bad_date_formats new file mode 100755 index 000000000..64930cf46 --- /dev/null +++ b/plugins/frontlinesms-core/do/i18n/check_for_bad_date_formats @@ -0,0 +1,19 @@ +#!/bin/bash +set -e +i18nDirectory=grails-app/i18n +echo "# Checking for bad date formats in $i18nDirectory..." +pushd $i18nDirectory +grep 'default\.date\.format' `ls *.properties` | groovy -e ' + System.in.text.eachLine { + parts = it.split(":default.date.format=") + file = parts[0]; dateFormat = parts[1] + try { + new java.text.SimpleDateFormat(dateFormat) + } catch(IllegalArgumentException) { + println "# Illegal date format in $file" + System.exit(1) + } + } +' +popd + diff --git a/plugins/frontlinesms-core/do/i18n/compare b/plugins/frontlinesms-core/do/i18n/compare new file mode 100755 index 000000000..a84f09124 --- /dev/null +++ b/plugins/frontlinesms-core/do/i18n/compare @@ -0,0 +1,60 @@ +#!/usr/bin/env groovy + +/* Script to compare grails internationalisation property files (or any property files in general) */ +if(args.size() < 2) { + println("# compares i18n file to master message.properties") + println("# usage: i18n-compare MASTER OTHER [APPLY-CHANGES] [NEW-FILE]") + println("# MASTER: The main property file, assumed to be correct") + println("# OTHER : The file to be compared to MASTER") + return +} + +def loadProps(filename) { + def props = new Properties() + new File(filename).withInputStream { stream -> props.load(stream); } + return props +} + +def compare(fileA, fileB) { + println "# Reporting on KEY differences between $fileA and $fileB..." + // read files into props + master = loadProps(fileA) + other = loadProps(fileB) + + // init lists + def missingKeys = (master.keySet().findAll { + (!other[it] || other[it].length() == 0) && + !(master[it] ==~ /\s*\{[0-9]\}\s*$/ ) + }) + def missingKeysSize = missingKeys.size() + def redundantKeys = other.keySet() - master.keySet() + def redundantKeysSize = redundantKeys.size() + def langName = other["language.name"] + + println "# RedundantKeys are..." + println "# \t${redundantKeys.join("\n# \t")}" + println "# Missing keys are..." + println "# \t${missingKeys.join("\n# \t")}" + def identicalValuesCount = compareValues(master, other) + double perc = ((master.size() - missingKeysSize - identicalValuesCount) / master.size()) * 100 + println "# Summary #" + println "# Redundant entries: $redundantKeysSize, MissingEntries: $missingKeysSize, Identical values: $identicalValuesCount" + println "# ${langName? langName + ' translation' : fileB} is ${perc.round(2)}% complete" + + return perc == 100 +} + +def compareValues(master, other) { + def identicalMap = master.findAll { k, v -> + if(master[k] == other[k]) { [k: other[k]] } + } + def identicalMapSize = identicalMap.keySet().size() + println "# Keys with identical Values are: " + println "#\t${identicalMap.keySet().join('\n#\t')}" + println "\n" + return identicalMapSize +} + +def perc = compare(args[0], args[1]) +System.exit perc == 100? 0: 1 + diff --git a/plugins/frontlinesms-core/do/i18n-update.groovy b/plugins/frontlinesms-core/do/i18n/fill_with_placeholders old mode 100644 new mode 100755 similarity index 55% rename from plugins/frontlinesms-core/do/i18n-update.groovy rename to plugins/frontlinesms-core/do/i18n/fill_with_placeholders index 1042959f5..ae47a7558 --- a/plugins/frontlinesms-core/do/i18n-update.groovy +++ b/plugins/frontlinesms-core/do/i18n/fill_with_placeholders @@ -1,9 +1,10 @@ +#!/usr/bin/env groovy + /* Script to compare grails internationalisation property files (or any property files in general) * and automatically update an incomplete file with missing keys from the master file (with TODOs). * */ -if(args.size() < 2) -{ +if(args.size() < 2) { println("updates a translation file with missing entries present in message.properties (or other 'master' file)") println("usage: groovy i18n-update.groovy MASTER OTHER [NEW-FILE]") println(" MASTER: The main property file, assumed to be complete") @@ -19,38 +20,39 @@ println "Applying changes to $targetFile" if (targetFile == args[1]) { def s = "* WARNING: This will overwrite the existing file $targetFile. Press ENTER to continue, or Ctrl+C to terminate *" - (1..s.length()).each { print('*') } - println "\n$s" - (1..s.length()).each { print('*') } - println "" - def input = System.in.withReader{ it.readLine() } + println('*' * s.size()) + println s + println('*' * s.size()) + def input = System.in.withReader { it.readLine() } } -String currentLine - def existingSlaveLines = [] def newSlaveLines = [] -slave.eachLine { line -> existingSlaveLines << line } +slave.eachLine("utf8") { line -> existingSlaveLines << line } -master.eachLine { masterLine -> +master.eachLine("utf8") { masterLine -> if(masterLine.isAllWhitespace() || masterLine.trim().startsWith("#")) { // This is a comment or whitespace, preserve it newSlaveLines << masterLine - } - else if (masterLine.contains('=')) { + } else if (masterLine.contains('=')) { // This is a property. Check if the other translation has it, and if not, copy it with a TODO - def key = masterLine.split('=')[0] - def masterValue = masterLine.split('=')[1] - - newSlaveLines << (existingSlaveLines.find { it.startsWith(key+"=") && it.split("=").size() > 1 } ?: masterLine.replaceFirst("=", "=TODO:")) - } - else { - // Something strange is going on, we should only have props, comments or whitespace! - } + def lineParts = masterLine.split('=', 2) + def key = lineParts[0].trim() + def masterValue = lineParts[1] + if(!(masterValue ==~ /\s*\{[0-9]\}\s*$/ )) { + def matchingSlaveLines = existingSlaveLines.findAll { it ==~ "^${key.replace('.', '\\.')}\\s*=.*" && it.split("=").size() > 1 } + if (matchingSlaveLines.size() == 1) + newSlaveLines << matchingSlaveLines[0] + else if (matchingSlaveLines.size() == 0) + newSlaveLines << "$key=TODO:$masterValue" + else throw new RuntimeException("There are duplicate entries in the target file for key ${key}") + } + } else throw new RuntimeException('Something strange is going on, we should only have props, comments or whitespace!') } -new File(targetFile).withWriter { out -> +new File(targetFile).withWriter("utf8") { out -> newSlaveLines.each { line -> out.writeLine(line) } } + diff --git a/plugins/frontlinesms-core/do/i18n/find_duplicates b/plugins/frontlinesms-core/do/i18n/find_duplicates new file mode 100755 index 000000000..8a9d0cee6 --- /dev/null +++ b/plugins/frontlinesms-core/do/i18n/find_duplicates @@ -0,0 +1,19 @@ +#!/bin/bash + +# Remove duplicate lines (key and value are equal) +sort $1 | uniq > temporary.tmp + +# Find keys that are not unique +doubleKeys=`awk -F"=" '{print $1}' temporary.tmp | sort | uniq -d` + +if [ -z "$doubleKeys" ] ; then + echo "# No duplicate keys found" +else + echo $doubleKeys > DoubleKeys.log + echo "# Duplicate keys:" + echo $doubleKeys + echo "----" + echo "# you can also find the outcome in DoubleKeys.log" +fi +rm temporary.tmp + diff --git a/plugins/frontlinesms-core/do/i18n/merge b/plugins/frontlinesms-core/do/i18n/merge new file mode 100755 index 000000000..404d439e9 --- /dev/null +++ b/plugins/frontlinesms-core/do/i18n/merge @@ -0,0 +1,73 @@ +#!/usr/bin/env groovy +@Grapes([ + @Grab(group='commons-configuration', module='commons-configuration', version='1.10'), + @Grab(group='org.apache.commons', module='commons-io', version='1.3.2') +]) +import org.apache.commons.io.FileUtils +import org.apache.commons.configuration.PropertiesConfigurationLayout +import org.apache.commons.configuration.PropertiesConfiguration + +def UTF8ENCODING = 'UTF-8' +def mergeLogFile = new File('target/i18nMergeLog.txt') +def mergeLog = new Date() as String +def log = { text -> + mergeLog += "\n${text}" + println text +} + +def getPropertiesForFile = { file -> + def props = new PropertiesConfiguration() + props.setListDelimiter(0 as char) + props.setFile(file) + props.load(FileUtils.openInputStream(file), UTF8ENCODING) + props +} + +def updateExistingLanguageFromFile = { existingFile, newFile -> + def existingProperties = getPropertiesForFile existingFile + def newProperties = getPropertiesForFile newFile + def existingKeys = existingProperties.keys + existingProperties.keys.each { key -> + (newProperties.containsKey(key)) ? existingProperties.setProperty(key, newProperties.getString(key)) : existingProperties.clearProperty(key) + } + + existingProperties.setEncoding(UTF8ENCODING) + def updatedTranslationLines = [] + existingProperties.keys.each { + if(existingProperties.getLayout().getComment(it)){ + updatedTranslationLines << "${existingProperties.getLayout().getComment(it)}".replaceAll("\\\\","\\\\\\\\").replaceAll("\n", "\\\\n") + } + updatedTranslationLines << "$it=${existingProperties.getString(it)}".replaceAll("\\\\","\\\\\\\\").replaceAll("\n", "\\\\n") + } + FileUtils.writeLines(existingFile, UTF8ENCODING, updatedTranslationLines) +} + +def updateLanguageFromFile = { newFile -> + // check if it exists in i18n folder + // if not, just copy it in and report this in log + // if it does, call update + def existingFile = new File("grails-app/i18n/${newFile.name}") + if(!existingFile.exists() ) { + FileUtils.copyFile(newFile, existingFile) + log "New translation ${existingFile.name} added" + } + else { + updateExistingLanguageFromFile existingFile, newFile + } +} + + + +def englishProperties = new PropertiesConfiguration() +englishProperties.setListDelimiter(0 as char) +englishProperties.setFile(new File('grails-app/i18n/messages.properties')) +englishProperties.load() +def otherFile = new File(args[0]) +if (otherFile.directory) { + otherFile.listFiles().each { file -> updateLanguageFromFile file } +} +else { + updateLanguageFromFile otherFile +} + +FileUtils.writeStringToFile(mergeLogFile, mergeLog) diff --git a/plugins/frontlinesms-core/do/i18n/report b/plugins/frontlinesms-core/do/i18n/report new file mode 100755 index 000000000..60f63a109 --- /dev/null +++ b/plugins/frontlinesms-core/do/i18n/report @@ -0,0 +1,12 @@ +#!/bin/bash +REPORT_DIR=target +mkdir -p $REPORT_DIR +REPORT_FILE=$REPORT_DIR/i18nreport.txt +echo "# Generating report on completeness of translations in grails-app/i18n/ ..." +for f in grails-app/i18n/messages_*.properties; do + echo "# Processing $f file.." + do/i18n/compare grails-app/i18n/messages.properties $f + echo "# ----" +done > >(tee -a $REPORT_FILE) +echo "# Report generated in $REPORT_FILE" + diff --git a/plugins/frontlinesms-core/do/jenkins/after b/plugins/frontlinesms-core/do/jenkins/after new file mode 100755 index 000000000..727712354 --- /dev/null +++ b/plugins/frontlinesms-core/do/jenkins/after @@ -0,0 +1,5 @@ +#!/bin/bash +set -e +do/codenarc_report_postprocess +do/js_unit_test_xml + diff --git a/plugins/frontlinesms-core/do/jenkins/before b/plugins/frontlinesms-core/do/jenkins/before new file mode 100755 index 000000000..dbdd6688e --- /dev/null +++ b/plugins/frontlinesms-core/do/jenkins/before @@ -0,0 +1,17 @@ +#!/bin/bash +set -e +do/check_for_ignored_tests +do/check_for_bad_template_renders +do/check_for_missing_fa_class +do/check_for_bad_resource_tags +do/check_for_setInterval_calls +do/check_for_static_simple_date_format +do/check_for_popups_without_loading_image +do/i18n/check_for_bad_date_formats +do/check_for_metaclass_in_non-unit_tests +do/functional_test_unsafe_text_calls_checker +do/functional_test_unflushed_saves_checker +do/jslint_report +do/csslint_report +do/simian_report --xml + diff --git a/plugins/frontlinesms-core/do/jenkins/simulate b/plugins/frontlinesms-core/do/jenkins/simulate new file mode 100755 index 000000000..b41a7a417 --- /dev/null +++ b/plugins/frontlinesms-core/do/jenkins/simulate @@ -0,0 +1,10 @@ +#!/bin/bash +set -e +echo "# Simulating steps of jenkins build on current version..." + +do/jenkins/before +grails test-app -coverage +do/jenkins/after + +echo "# Jenkins build simulation completed successfully." + diff --git a/plugins/frontlinesms-core/do/js_unit_test b/plugins/frontlinesms-core/do/js_unit_test new file mode 100755 index 000000000..6c5c31007 --- /dev/null +++ b/plugins/frontlinesms-core/do/js_unit_test @@ -0,0 +1,53 @@ +#!/bin/bash +ERROR_FILE=target/js_unit_test.log.err + +checkRequiredNpmLibs=true +while [[ $1 == "--"* ]]; do + if [[ $1 == "--no-npm-check" ]]; then + checkRequiredNpmLibs=false + fi + shift +done + +if [ -z "$@" ]; then + pushd test/js + TESTS=`ls *_tests.js | sed -e "s/_tests.js//"` + popd +else + TESTS="$@" +fi +echo "# Tests to run: $TESTS" + +function err { + 1>&2 echo "$@" +} + +function check_npm_module_available { + MODULE_NAME=$1 + err "# Checking for NPM module: $MODULE_NAME..." + INSTALL_COUNT=`(npm list; npm -g list) | grep -c "^[├└]─[─┬] $MODULE_NAME\@"` + if [ 0 -eq $INSTALL_COUNT ]; then + err "# NPM module missing: $MODULE_NAME. To install globally, please run 'sudo npm i -g $MODULE_NAME'" + err "# Installing module locally..." + npm install $MODULE_NAME + fi + err "# NPM Module found: $MODULE_NAME" +} +if $checkRequiredNpmLibs; then + check_npm_module_available fs + check_npm_module_available jquery + check_npm_module_available jsdom + check_npm_module_available qunit +fi + +function run_test() { + TARGET=$1 + echo "# Running tests for $TARGET..." + qunit -c "web-app/js/${TARGET}.js" -d `ls test/js/lib/*.js` -t "test/js/${TARGET}_tests.js" 2> $ERROR_FILE + grep -v "^ERROR.*" $ERROR_FILE +} + +for TEST in $TESTS; do + run_test $TEST +done + diff --git a/plugins/frontlinesms-core/do/js_unit_test_lib/xml_filter.groovy b/plugins/frontlinesms-core/do/js_unit_test_lib/xml_filter.groovy new file mode 100755 index 000000000..a09430a97 --- /dev/null +++ b/plugins/frontlinesms-core/do/js_unit_test_lib/xml_filter.groovy @@ -0,0 +1,40 @@ +#!/usr/bin/env groovy +import groovy.xml.StreamingMarkupBuilder + +BufferedReader sin = new BufferedReader(new InputStreamReader(System.in)) +def unfiltered = '' + sin.text + '' + +def slurp = new XmlParser().parseText(unfiltered) +def suites = slurp.testsuites + +Node root = new Node(null, 'testsuites') +// Reverse the suites so we get the last-run first - this should give us proper timings for +// the tests +def alreadySeen = [] +suites.reverse().each { testsuites -> + testsuites.children().each { testsuite -> + def classname = 'js.' + testsuite.attributes().name + if(classname in alreadySeen) return; + alreadySeen << classname + def expectedChildren = ['properties', 'system-out', 'system-err'] + testsuite.attributes().name = classname + testsuite.children().each { testcase -> + expectedChildren.remove(testcase.name()) + if(testcase.name() == 'testcase') { + def att = testcase.attributes() + ['total', 'failed'].each { att.remove(it) } + att.classname = classname + } + } + expectedChildren.each { name -> + testsuite.appendNode(name) + } + root.append testsuite + } +} + +def writer = new StringWriter() +new XmlNodePrinter(new PrintWriter(writer)).print(root) +println '' +println writer.toString() + diff --git a/plugins/frontlinesms-core/do/js_unit_test_lib/xml_split b/plugins/frontlinesms-core/do/js_unit_test_lib/xml_split new file mode 100755 index 000000000..df422edf3 --- /dev/null +++ b/plugins/frontlinesms-core/do/js_unit_test_lib/xml_split @@ -0,0 +1,33 @@ +#!/bin/bash +# Take input of unit test XML and split into separate files + +TARGET_DIRECTORY=target/test-reports + +OUTPUT_FILE="discard.me" +function output { + #echo "Output -> $OUTPUT_FILE: $@" + echo "$@" >> $TARGET_DIRECTORY/$OUTPUT_FILE +} + +DEFAULT_OUTPUT_FILE_COUNT=0 +while IFS="" read -r LINE; do + # check if line is the start of a new file, and change OUTPUT_FILE if appropriate + if echo $LINE | grep -q '^' + fi + + # check if line is metadata not required in output file, and discard as appropriate + if [[ "discard.me" == "$OUTPUT_FILE" ]] || echo "$LINE" | grep -q ''; then + continue + fi + + # output useful output to the specific file + output "$LINE" +done + diff --git a/plugins/frontlinesms-core/do/js_unit_test_xml b/plugins/frontlinesms-core/do/js_unit_test_xml new file mode 100755 index 000000000..c6d35137f --- /dev/null +++ b/plugins/frontlinesms-core/do/js_unit_test_xml @@ -0,0 +1,7 @@ +#!/bin/bash +set -e +REPORT_DIR=target/test-reports +mkdir -p $REPORT_DIR + +do/js_unit_test | sgrep '("")' | sed -e 's:::' | do/js_unit_test_lib/xml_filter.groovy | tee $REPORT_DIR/TESTS-javascript-TestSuites.xml | do/js_unit_test_lib/xml_split + diff --git a/plugins/frontlinesms-core/do/jslint b/plugins/frontlinesms-core/do/jslint index 9d659b350..b82d81a2f 100755 --- a/plugins/frontlinesms-core/do/jslint +++ b/plugins/frontlinesms-core/do/jslint @@ -30,7 +30,7 @@ else fi echo "# Linting: $FILES" -jslint --white --sloppy --white --browser --nomen --plusplus --forin --undef $EXTRA_FLAGS \ +jslint --white --sloppy --white --browser --plusplus --forin --undef $EXTRA_FLAGS \ --predef $ \ --predef jQuery \ --predef url_root \ diff --git a/plugins/frontlinesms-core/do/jslint_for_gsp b/plugins/frontlinesms-core/do/jslint_for_gsp index eda2868dc..893548878 100755 --- a/plugins/frontlinesms-core/do/jslint_for_gsp +++ b/plugins/frontlinesms-core/do/jslint_for_gsp @@ -3,8 +3,12 @@ # TODO load predefs from web-app/js global vars TEMP_JS=target/temp.js -do/extract_js_from_gsps +do/extract_js_from_gsps $@ echo "# Linting..." do/jslint $TEMP_JS +if [ -z "$@" ]; then + # special case - these scripts are in GSPs but aren't enclosed in tags. + do/jslint grails-app/views/webconnection/*/_scripts.gsp +fi echo "# Lint complete." diff --git a/plugins/frontlinesms-core/do/make_feature_branch b/plugins/frontlinesms-core/do/make_feature_branch new file mode 100755 index 000000000..68a23cff0 --- /dev/null +++ b/plugins/frontlinesms-core/do/make_feature_branch @@ -0,0 +1,16 @@ +#!/bin/bash +count=`git status --porcelain | wc -c` +if [ $count -ne 0 ]; then + echo "git status is not clean, stage and commit changes first" + exit 1 +fi +if [ -z $1 ]; then + echo "This script takes one argument which is the name of the branch e.g do/make_feature_branch CORE-XYZ" + exit 1 +fi +git checkout master +git pull +git checkout -b $1 +git push -u origin $1 +wget -O /dev/null "http://192.168.0.200/exec_script.php?user=jenkins&cmd=clone_jenkins_job&args=frontlinesms2-core-master%20$1%20frontlinesms2-$1" +exit 0 diff --git a/plugins/frontlinesms-core/do/migration b/plugins/frontlinesms-core/do/migration new file mode 100644 index 000000000..e77b9b029 --- /dev/null +++ b/plugins/frontlinesms-core/do/migration @@ -0,0 +1,38 @@ +#!bin/bash + +#Generate initial database in prod mode without any data in background +grails prod run-app + +RESPONSE="000" +SERVER_PORT=8080 +CONTEXT_PATH=frontlinesms-core +PING_URL=http://localhost:$SERVER_PORT$CONTEXT_PATH/status/show +echo "# Waiting for server to start" +echo "# Ping URL: $PING_URL" +until [ "$RESPONSE" -ne "000" ]; do + echo "# Pinging $PING_URL..." + RESPONSE=`curl -o /dev/null --silent --head --write-out '%{http_code}' $PING_URL` || echo "Setting response seems to give an error code" + echo "# Pinged server at $PING_URL and got response: $RESPONSE" + sleep 10 +done + +echo "# Final server response: $RESPONSE" +if [ "$RESPONSE" -eq "200" ]; then + echo "# Started FrontlineSMS successfully \\o/" + + #Kill grails and do migrations + do/kill-grails + #enter changelog details + echo "Enter name of the changelog" + read -e CHANGELOG_NAME + grails prod gorm-diff $CHANGELOG_NAME.groovy + less grails-app/migrations/$CHANGELOG_NAME.groovy + EXIT_CODE=0 +else + echo "# Error starting FrontlineSMS" + EXIT_CODE=1 +fi + + + +#view changelog diff --git a/plugins/frontlinesms-core/do/migrations b/plugins/frontlinesms-core/do/migrations new file mode 100644 index 000000000..e77b9b029 --- /dev/null +++ b/plugins/frontlinesms-core/do/migrations @@ -0,0 +1,38 @@ +#!bin/bash + +#Generate initial database in prod mode without any data in background +grails prod run-app + +RESPONSE="000" +SERVER_PORT=8080 +CONTEXT_PATH=frontlinesms-core +PING_URL=http://localhost:$SERVER_PORT$CONTEXT_PATH/status/show +echo "# Waiting for server to start" +echo "# Ping URL: $PING_URL" +until [ "$RESPONSE" -ne "000" ]; do + echo "# Pinging $PING_URL..." + RESPONSE=`curl -o /dev/null --silent --head --write-out '%{http_code}' $PING_URL` || echo "Setting response seems to give an error code" + echo "# Pinged server at $PING_URL and got response: $RESPONSE" + sleep 10 +done + +echo "# Final server response: $RESPONSE" +if [ "$RESPONSE" -eq "200" ]; then + echo "# Started FrontlineSMS successfully \\o/" + + #Kill grails and do migrations + do/kill-grails + #enter changelog details + echo "Enter name of the changelog" + read -e CHANGELOG_NAME + grails prod gorm-diff $CHANGELOG_NAME.groovy + less grails-app/migrations/$CHANGELOG_NAME.groovy + EXIT_CODE=0 +else + echo "# Error starting FrontlineSMS" + EXIT_CODE=1 +fi + + + +#view changelog diff --git a/plugins/frontlinesms-core/do/optimise_images b/plugins/frontlinesms-core/do/optimise_images index f1ffd7823..0a93fac8f 100755 --- a/plugins/frontlinesms-core/do/optimise_images +++ b/plugins/frontlinesms-core/do/optimise_images @@ -1,7 +1,13 @@ #!/bin/bash echo "Optimising PNGs with PNGout..." -find web-app/ -name "*.png" -print0 -type f | xargs --null -n 1 pngout -r +if [ -z "$@" ]; then + IMAGE_LOCATION=web-app/ +else + IMAGE_LOCATION="$@" +fi +echo "Searching for images in $IMAGE_LOCATION..." +find $IMAGE_LOCATION -name "*.png" -print0 -type f | xargs --null -n 1 pngout -r echo "Image optimisation complete." diff --git a/plugins/frontlinesms-core/do/plugin/release b/plugins/frontlinesms-core/do/plugin/release new file mode 100755 index 000000000..164c78701 --- /dev/null +++ b/plugins/frontlinesms-core/do/plugin/release @@ -0,0 +1,76 @@ +#!/bin/bash +set -e + +tagPrefix="v" +orphanBranch=false +while [[ $1 == "--"* ]]; do + if [[ $1 == "--orphan" ]]; then + orphanBranch=true + fi + if [[ $1 == "--tag-prefix" ]]; then + shift + tagPrefix=$1 + fi + shift +done + +previousVersion=$(groovy -e "$(grep version *GrailsPlugin.groovy); println version") +echo "# Got old plugin version as: $previousVersion" + +releaseVersion=$(echo $previousVersion | sed s/-SNAPSHOT//) +read -p "# Enter version for release [$releaseVersion]: " manualReleaseVersion +if [ -n "$manualReleaseVersion" ]; then + releaseVersion="$manualReleaseVersion" +fi +echo "# Using new version: $releaseVersion" + +nextVersion=$(expr $releaseVersion) +nextVersionHead=$(echo $nextVersion | sed -E 's/^(.*)\.[0-9]+$/\1/') +nextVersionTail=$(echo $nextVersion | sed -E 's/^.*\.([0-9]+)$/\1/') +nextVersionTail=$(($nextVersionTail + 1)) +nextVersion="${nextVersionHead}.${nextVersionTail}-SNAPSHOT" +read -p "# Enter new version for development [$nextVersion]: " manualNextVersion +if [ -n "$manualNextVersion" ]; then + nextVersion="$manualNextVersion" +fi +echo "# Next SNAPSHOT version: $nextVersion" + +function grails_change_plugin_version { + _prev="$1" + _next="$2" + sed -i -E -e "s/def\s+version\s*=\s*['\"]$_prev['\"]/def version = '$_next'/g" *GrailsPlugin.groovy +} + +function git_commit_all_changes { + _commitMessage="$1" + git status + read -p "# Committing all changes with message: $_commitMessage [enter] " + # Add new files to git + git add . + # --all will also remove deleted files + git commit --all -m"$_commitMessage" +} + +orphanBranchName= +if $orphanBranch; then + orphanBranchName="release-${tagPrefix}${releaseVersion}" + echo "# Creating new orphaned branch: $orphanBranchName" + git checkout --orphan $orphanBranchName + git commit -m"Branch for release of ${tagPrefix}${releaseVersion}" +fi + +grails_change_plugin_version $previousVersion $releaseVersion +${0%/*}/../build_deploy_plugin --m2-deploy # TODO should support binary here too +git_commit_all_changes "[RELEASE: $releaseVersion]" +git tag "${tagPrefix}${releaseVersion}" +if $orphanBranch; then + echo "# Pushing changes to remote for orhan branch: $orhpanBranchName" + git push origin $orphanBranchName + echo "# Checking out master so SNAPSHOT version rolling happens there instead of on orphan branch..." + git checkout master +fi +grails_change_plugin_version $previousVersion $nextVersion +git_commit_all_changes "[POST-RELEASE: $releaseVersion -> $nextVersion]" +git push +git push --tags + diff --git a/plugins/frontlinesms-core/do/release b/plugins/frontlinesms-core/do/release index 9be40b090..8a54109cc 100755 --- a/plugins/frontlinesms-core/do/release +++ b/plugins/frontlinesms-core/do/release @@ -1,43 +1,81 @@ #!/bin/bash -echo "Building new release" -echo "Checking for clean repository..." +# FOR AUTOMATED BUILDING OF NEW RELEASES OF FRONTLINESMS +# Requirements: Groovy, Gradle. Currently only works on linux. + +echo "### Building FrontlineSMS v2 release" +read -sn 1 -p "## WARNING ## This script will make modifications to your working directory, and commit these changes. It will also create a tag for the newly built version. Press any key to continue or Ctrl+C to quit..." +echo "" + +# Read Application.properties to find release version +APP_VERSION=`sed '/^\#/d' application.properties | grep 'app.version' | tail -n 1 | cut -d "=" -f2-` +echo "# Read current app version as ${APP_VERSION}" +APP_VERSION_SNAPSHOT_SUBSTRING=${APP_VERSION:(-9)} + +# Check if app version is recognised +if [ "$APP_VERSION_SNAPSHOT_SUBSTRING" != "-SNAPSHOT" ]; + then echo "# FAILURE: application version ending with -SNAPSHOT expected" && exit 1 +fi + +# Drop -SNAPSHOT +APP_VERSION=${APP_VERSION%$APP_VERSION_SNAPSHOT_SUBSTRING} +echo "after dropping -SNAPSHOT, app version is now ${APP_VERSION}" + +# Check if build is an RC +echo "$app_version" | grep -qi "rc" +RC_FLAG=$? + +# Ensure git repo is clean +echo "# Checking for clean repository..." if test -n "$(git status --porcelain)" then - echo "FAILURE: Your repository is not clean. This script can only build from a clean repo" + echo "# FAILURE: Your repository is not clean. This script can only build from a clean repo" exit 1 fi -# TODO: i18n -# TODO: roll version numbers to non-snapshot -echo "Rolling version numbers to non-snapshot" -do/remove_snapshot_from_install_resource_directories +# Prompt for post-build version +echo "Please enter the name of the version as it should be after build (e.g. 2.4.3-SNAPSHOT)" +read POST_BUILD_VERSION -# TODO: do builds -echo "Building installers.." -grails prod BuildInstallers +# Drop -SNAPSHOT from application.properties +sed -i "s/\(app\.version=\).*\$/\1${APP_VERSION}/" application.properties -# TODO: test build in appropriate format for machine -#### This can reuse elements of build_uninstall_install_test +# Run migration tests +pushd test/migration +gradle run +if [ $? -eq 0 ]; + then echo "# FAILURE: migration tests did not pass, ensure you have valid migration set up" && exit 1 +fi +popd -# TODO: commit -#### git commit -m "Build for 2.a.b" -#### git push +# Roll version numbers to non-snapshot +echo "# Rolling version numbers to non-snapshot" +do/remove_snapshot_from_install_resource_directories -# TODO: tag commit -#### git tag frontlinesms2.a.b -#### git push +# Build installers +echo "# Building installers.." +grails prod BuildInstallers -# TODO: prompt user for new snapshot version number -#### prompt for new snapshot number +# test build in appropriate format for machine (TODO: currently assumes linux) +do/uninstall_install_test +if [ $? -ne 0 ]; + then echo "# FAILURE: build test failed, see output to debug" && exit 1 +fi -# TODO: roll version to new snapshot -#### similar to do/remove_snapshot_from_install_resource_directories +# Commit +echo "Committing and tagging build" +git add -A +git commit -m "Built ${APP_VERSION} (committed using do/release script)" +git tag frontlinesms2.$APP_VERSION -# TODO: commit with new snapshot version -#### git commit -m "Rolled version numbers to 2.x.y snapshot after 2.a.b release" +# Roll application.properties to post-build version +sed -i "s/\(app\.version=\).*\$/\1${POST_BUILD_VERSION}/" application.properties +git add -A +git commit -m "2.${POST_BUILD_VERSION} ready for dev after 2.${APP_VERSION} release (committed using do/release script)" -# TODO: git push --tags -# TODO: upload builds -#### CORE-1440 +# Upload builds +do/deploy_builds +# Push (uncomment the lines below and delete the TODO: when this script passes QA) +git push +git push --tags diff --git a/plugins/frontlinesms-core/do/rm_plugin_excluded_files b/plugins/frontlinesms-core/do/rm_plugin_excluded_files new file mode 100755 index 000000000..ed638094f --- /dev/null +++ b/plugins/frontlinesms-core/do/rm_plugin_excluded_files @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +NONEWLINES=$(tr -d '\n' < FrontlinesmsCoreGrailsPlugin.groovy) +FILE_LIST=$(echo $NONEWLINES | grep -o '\[.*\]') +ans=`echo $FILE_LIST | grep -o 'grai.*'` +for i in $ans +do + FILE=$(echo $i | tr -d ',' | tr -d '\"' | tr -d ']' | tr -d '[') + echo "# Deleting from core $FILE" + rm $FILE +done diff --git a/plugins/frontlinesms-core/do/routes_to_svg b/plugins/frontlinesms-core/do/routes_to_svg new file mode 100755 index 000000000..a310cfabb --- /dev/null +++ b/plugins/frontlinesms-core/do/routes_to_svg @@ -0,0 +1,33 @@ +#!/bin/bash +set -e +source bails/head + +error_handler() { + log "There was an error." + log "You may need to install graphviz, e.g." + log " sudo apt-get install graphviz" + exit $1 +} +trap error_handler ERR + +launch_browser=false +while [[ "$1" == --* ]]; do + if [[ "$1" == "--launch-browser" ]]; then + launch_browser=true + fi + shift +done + +tmp=$(pwd)/tmp +mkdir -p $tmp +target=${tmp}/routes.svg +log "Generating SVG for routes..." +dot -o${target} -Tsvg asdf/routes.dot +log "SVG generated at ${target}." + +if $launch_browser; then + log "Launching browser..." + firefox file://$target + log "Done." +fi + diff --git a/plugins/frontlinesms-core/do/simian_report b/plugins/frontlinesms-core/do/simian_report new file mode 100755 index 000000000..a6b3cf794 --- /dev/null +++ b/plugins/frontlinesms-core/do/simian_report @@ -0,0 +1,19 @@ +#!/bin/bash +set -e +if [[ -z $SIMIAN_JAR ]]; then + echo "# Environment variable SIMIAN_JAR not set. Simian report will not run." + exit 0 +fi + +if [[ $1 == "--xml" ]]; then + simianFormat='-formatter=xml:target/simian.xml' +fi + +echo "# Using simian jar at: $SIMIAN_JAR..." +echo "# I hope you've paid for a licence..." + +set +e +java -jar $SIMIAN_JAR $simianFormat -language=groovy "src/groovy/**/*.groovy" "grails-app/**/*.groovy" "test/**/*.groovy" +echo "# Simian completed with exit code: $?" +set -e + diff --git a/plugins/frontlinesms-core/do/test_migrations b/plugins/frontlinesms-core/do/test_migrations new file mode 100755 index 000000000..6c201a130 --- /dev/null +++ b/plugins/frontlinesms-core/do/test_migrations @@ -0,0 +1,14 @@ +#!/bin/bash +set -e +TEMP_DIR=../../../frontlinesms2-db-migrate-test-temp +echo "# Checking if $TEMP_DIR folder exists already..." +TODO + +echo "# Running the tests..." +TODO + +echo "# Cleaning temp dir $TEMP_DIR..." +rm -rf $TEMP_DIR + +echo "# migration test script complete." + diff --git a/plugins/frontlinesms-core/do/uninstall_install_test b/plugins/frontlinesms-core/do/uninstall_install_test new file mode 100755 index 000000000..59c84cbb6 --- /dev/null +++ b/plugins/frontlinesms-core/do/uninstall_install_test @@ -0,0 +1,69 @@ +#!/bin/bash +set -e + +echo "#############################################" +echo "## This script aims to test new installers ##" +echo "## 1. uninstall old installation ##" +echo "## 2. install new version ##" +echo "## 3. run new version ##" +echo "## 4. check that the server is working! ##" +echo "#############################################" + +PROJECT_DIR=../frontlinesms-core +INSTALL_DIR=~/frontlinesms2 +ALL_USER_INSTALL_DIR=/opt/frontlinesms2 +SETTINGS_DIR=~/.frontlinesms2 +SERVER_PORT=8130 +CONTEXT_PATH= + +echo "# uninstall old version" +$INSTALL_DIR/uninstall -q || echo "# Could not run uninstaller in $INSTALL_DIR" +$ALL_USER_INSTALL_DIR/uninstall -q || echo "# Could not run uninstaller in $ALL_USER_INSTALL_DIR" + +echo "# remove old install directory" +rm -rf $INSTALL_DIR/ || rm -rf $ALL_USER_INSTALL_DIR || echo "# Could not remove installation directory as it did not exist" + +echo "# remove old settings/database directory" +rm -rf $SETTINGS_DIR/ + +echo "# install new build" +$PROJECT_DIR/install/target/install4j/*.sh -q || echo "There was an error running the installer TODO supply a valid email address on commandline so this does not fail." + +echo "# run new install in background" +$INSTALL_DIR/*_Launcher || $ALL_USER_INSTALL_DIR/*_Launcher & +LAUNCHER_PROCESS_ID=`jobs -p` +LAUNCHER_PROCESS_GROUP_ID=`ps -j --pid $LAUNCHER_PROCESS_ID | tail -n1 | grep -Po -m2 "\w+" | grep -Pm2 "\w+" | tail -n1` +jobs +echo "Launched process ID: $LAUNCHER_PROCESS_ID with group $LAUNCHER_PROCESS_GROUP_ID" + +RESPONSE="000" +PING_URL=http://localhost:$SERVER_PORT$CONTEXT_PATH/status/show +echo "# Waiting for server to start" +echo "# Ping URL: $PING_URL" +until [ "$RESPONSE" -ne "000" ]; do + echo "# Pinging $PING_URL..." + RESPONSE=`curl -o /dev/null --silent --head --write-out '%{http_code}' $PING_URL` || echo "Setting response seems to give an error code" + echo "# Pinged server at $PING_URL and got response: $RESPONSE" + sleep 10 +done + +echo "# Final server response: $RESPONSE" +if [ "$RESPONSE" -eq "200" ]; then + echo "# Started FrontlineSMS successfully \\o/" + EXIT_CODE=0 +else + echo "# Error starting FrontlineSMS" + EXIT_CORE=1 +fi + +echo "# Killing FrontlineSMS instance" +CHILD_PROCESSES=`ps h -o pid -$LAUNCHER_PROCESS_GROUP_ID | tac | tail -n +2` +echo "# Detected processes: $CHILD_PROCESSES" +echo "# Excluding process: $LAUNCHER_PROCESS_GROUP_ID" +CHILD_PROCESSES=`echo "$CHILD_PROCESSES" | sed s/$LAUNCHER_PROCESS_GROUP_ID//` +echo "# Killing child processes: $CHILD_PROCESSES" +kill -TERM $CHILD_PROCESSES || echo "Kill doesn't give us a nice exit code BTW" + +echo "Exiting with code: $EXIT_CODE" +exit $EXIT_CODE + diff --git a/plugins/frontlinesms-core/do/upload_grails_plugin b/plugins/frontlinesms-core/do/upload_grails_plugin new file mode 100755 index 000000000..93a120ed7 --- /dev/null +++ b/plugins/frontlinesms-core/do/upload_grails_plugin @@ -0,0 +1,63 @@ +#!/bin/bash +set -e + +function TODO { + echo "Not yet implemented: $@" + echo "Exiting script." + exit 1; +} + +function show_usage { + cat << EOF +$0 +EOF +} + +function to_camel_case { + echo $@ | sed -r "s/[-\.](\w)/\U\1/g" +} + +function capitalise { + echo $@ | sed -r "s/(^|\s)(\w)/\1\U\2\E/g" +} + +PLUGIN_PACKAGE=org.grails.plugins +PLUGIN_NAME=`grep app.name application.properties | cut -d= -f2` +PLUGIN_NAME_CAMELCASE=`capitalise $(to_camel_case $PLUGIN_NAME)` +echo "# Got plugin camelcase name as: $PLUGIN_NAME_CAMELCASE" +PLUGIN_CLASS_FILE=${PLUGIN_NAME_CAMELCASE}GrailsPlugin.groovy +echo "# Plugin class file: $PLUGIN_CLASS_FILE" +PLUGIN_VERSION=`grep -E "def\s+version\s+=" $PLUGIN_CLASS_FILE | cut -d= -f2 | sed -E "s:[' ]::g"` +FTP_ADDRESS=dev.frontlinesms.com +FTP_PATH=`echo $PLUGIN_PACKAGE | sed -E "s:\.:/:g"` +FTP_USERNAME=$FRONTLINESMS_MAVEN_USERNAME +FTP_PASSWORD=$FRONTLINESMS_MAVEN_PASSWORD +ZIP_NAME_NEW="$PLUGIN_NAME-$PLUGIN_VERSION.zip" +ZIP_NAME_OLD="grails-$ZIP_NAME_NEW" + +echo "# Uploading plugin $PLUGIN_PACKAGE:$PLUGIN_NAME:$PLUGIN_VERSION to site $FTP_ADDRESS/$FTP_PATH..." + +echo "# Copying zip from $ZIP_NAME_OLD to $ZIP_NAME_NEW..." +cp $ZIP_NAME_OLD $ZIP_NAME_NEW +echo "# Zip copied." + +if [ -z "$PLUGIN_NAME" ]; then + show_usage + exit 1 +fi +if [ -z "$PLUGIN_VERSION" ]; then + show_usage + exit 1 +fi + +echo "# Using ftp credentials: $FTP_USERNAME:$FTP_PASSWORD" +ftp -n $FTP_ADDRESS << EOF +user $FTP_USERNAME $FTP_PASSWORD +cd org/grails/plugins/$PLUGIN_NAME +mkdir $PLUGIN_VERSION +cd $PLUGIN_VERSION +put $ZIP_NAME_NEW +exit +EOF + +echo "# Plugin uploaded" diff --git a/plugins/frontlinesms-core/do/workOn b/plugins/frontlinesms-core/do/workOn new file mode 100755 index 000000000..9f1d92eb6 --- /dev/null +++ b/plugins/frontlinesms-core/do/workOn @@ -0,0 +1,25 @@ +#!/bin/bash +if [ -z "$1" ]; then + echo "# Usage: do/workOn TOOLS-XYZ" + exit 1 +fi +echo "Is $1 a User Story [s] or a Bug [b]? [s/b]" +read storyFlag + +if [ "$storyFlag" == "s" ]; then + echo "Treating new ticket as story, will branch from master" + git co master +elif [ "$storyFlag" == "b" ]; then + echo "Treating new ticket as bug, will branch from core-4.x maintenance branch" + git co core-4.x +else + echo "invalid input, exiting" + exit 1 +fi + +git pull +git co -b $1 +git push -u origin $1 + +echo "Created new branch $1 on origin. Happy coding!" + diff --git a/plugins/frontlinesms-core/grails-app/conf/ApiUrlMappings.groovy b/plugins/frontlinesms-core/grails-app/conf/ApiUrlMappings.groovy deleted file mode 100644 index 2f6fe10b0..000000000 --- a/plugins/frontlinesms-core/grails-app/conf/ApiUrlMappings.groovy +++ /dev/null @@ -1,10 +0,0 @@ -class ApiUrlMappings { - static mappings = { - "/" controller:'dashboard' // TODO remap this using a controller so the URL is written properly - "/api/1/$entityClassApiUrl/$entityId/$secret?" controller:'api' - "/dashboard/$action?" controller:'dashboard' - "/login/$action?" controller:'login' - "/logout/$action?" controller:'logout' - } -} - diff --git a/plugins/frontlinesms-core/grails-app/conf/BuildConfig.groovy b/plugins/frontlinesms-core/grails-app/conf/BuildConfig.groovy index c72e817b4..0695f1c76 100644 --- a/plugins/frontlinesms-core/grails-app/conf/BuildConfig.groovy +++ b/plugins/frontlinesms-core/grails-app/conf/BuildConfig.groovy @@ -2,6 +2,7 @@ grails.servlet.version = "2.5" // Change depending on target container complianc grails.project.class.dir = "target/classes" grails.project.test.class.dir = "target/test-classes" grails.project.test.reports.dir = "target/test-reports" +grails.project.work.dir = 'target' grails.project.target.level = 1.6 grails.project.source.level = 1.6 //grails.project.war.file = "target/${appName}-${appVersion}.war" @@ -13,8 +14,6 @@ environments { } grails.project.dependency.resolution = { - def gebVersion = '0.7.2' - // inherit Grails' default dependencies inherits("global") { // uncomment to disable ehcache @@ -30,10 +29,8 @@ grails.project.dependency.resolution = { grailsPlugins() mavenLocal() - mavenRepo "http://192.168.0.200:8081/artifactory/simple/super-repo/" - grailsRepo "http://192.168.0.200:8081/artifactory/simple/super-repo/" - mavenRepo 'http://dev.frontlinesms.com/m2repo/' mavenCentral() + mavenRepo 'http://dev.frontlinesms.com/m2repo/' grailsCentral() @@ -46,56 +43,55 @@ grails.project.dependency.resolution = { } dependencies { - // specify dependencies here under either 'build', 'compile', 'runtime', 'test' or 'provided' scopes eg. - - // runtime 'mysql:mysql-connector-java:5.1.16' - def seleniumVersion = '2.25.0' def camel = { - def camelVersion = "2.9.2" + def camelVersion = "2.9.4" "org.apache.camel:camel-$it:$camelVersion" } // TEST test camel('test') - test "org.codehaus.geb:geb-spock:$gebVersion" - test "org.seleniumhq.selenium:selenium-support:$seleniumVersion" - test "org.seleniumhq.selenium:selenium-firefox-driver:$seleniumVersion" - test "org.seleniumhq.selenium:selenium-remote-driver:$seleniumVersion" // TODO this should be included in compile for TEST and DEV scopes, and excluded for PRODUCTION compile 'net.frontlinesms.test:hayescommandset-test:0.0.4' // COMPILE - compile 'net.frontlinesms.core:camel-smslib:0.0.5' - ['mail', 'http'].each { compile camel(it) } + //compile 'net.frontlinesms.core:smslib:1.1.4' + compile('net.frontlinesms.core:camel-smslib:0.0.7') { + //excludes 'smslib' + } + ['mail', 'http', 'smpp'].each { compile camel(it) } compile 'net.frontlinesms.core:serial:1.0.1' - compile 'net.frontlinesms.core:at-modem-detector:0.8' - runtime 'org.rxtx:rxtx:2.1.7' - runtime 'javax.comm:comm:2.0.3' + compile 'net.frontlinesms.core:at-modem-detector:0.10' + compile 'org.rxtx:rxtx:2.1.7' + compile 'javax.comm:comm:2.0.3' + compile('org.codehaus.groovy.modules.http-builder:http-builder:0.6') { + excludes "commons-logging", "xml-apis", "groovy" + } + compile 'com.googlecode.libphonenumber:libphonenumber:4.3' + compile 'com.googlecode.ez-vcard:ez-vcard:0.9.0' + compile 'org.apache.httpcomponents:httpclient:4.3' + compile 'org.apache.commons:commons-math:2.0' } plugins { + test ':frontlinesms-grails-test:0.19' + compile ":hibernate:$grailsVersion" - runtime ":database-migration:1.0" - runtime ":jquery:1.7.1" - runtime ':jquery-ui:1.8.15' - runtime ":resources:1.1.6" - - runtime ":export:1.1" - runtime ":markdown:1.0.0.RC1" - runtime ':routing:1.2.2' - runtime ":csv:0.3.1" - compile ":quartz2:0.2.3-frontlinesms" - - test ":code-coverage:1.2.5" - test ":codenarc:0.17" - test ":spock:0.6" - test ":geb:$gebVersion" - - test ':build-test-data:2.0.2' - test ':remote-control:1.2' - compile(':functional-test-development:0.9.3') { - exclude 'hibernate' + compile ":database-migration:1.0" + compile ":jquery:1.7.1" + compile ':jquery-ui:1.8.15' + compile ':resources:1.2' + + compile ":export:1.1" + compile ":markdown:1.0.0.RC1" + compile ':routing:1.2.2-camel-2.9.4' + compile ":csv:0.3.1" + compile ":quartz2:2.1.6.2" + + compile ':platform-core:1.0.RC3-frontlinesms' + + compile ":flashier-messages:1.0", { + excludes 'spock' } // Uncomment these (or add new ones) to enable additional resources capabilities @@ -103,26 +99,60 @@ grails.project.dependency.resolution = { //runtime ":cached-resources:1.0" //runtime ":yui-minify-resources:0.1.4" - build(":tomcat:$grailsVersion") { + build ":release:$grailsVersion", { + excludes 'http-builder' + export = false + } + build ":tomcat:$grailsVersion", { + export = false + } + build ':bails:0.6' + compile ":font-awesome-resources:4.0.3.0" + + // FIXES as per http://stackoverflow.com/questions/14581009/unknown-plugin-included-in-war-when-building + test ':build-test-data:2.0.5', { + export = false + } + test ':remote-control:1.4', { + export = false + } + test ':geb:0.7.2', { export = false } } } coverage { + enabledByDefault = false + exclusions = ["**/*Spec"] + xml = true } codenarc { - reportName = 'target/analysis-reports/codenarc.xml' - reportType = 'xml' + reports = { + xmlReport('xml') { + outputFile = 'target/analysis-reports/codenarc.xml' + title = 'CodeNarc Report' + } + htmlReport('html') { + outputFile = 'target/analysis-reports/codenarc.html' + title = 'CodeNarc Report' + } + } systemExitOnBuildException = false // NB these numbers should be LOWERED over time as code quality should be INCREASING maxPriority1Violations = 0 - maxPriority2Violations = 250 + maxPriority2Violations = 260 maxPriority3Violations = 500 properties = { GrailsPublicControllerMethod.enabled = false + GrailsDomainHasToString.enabled = false + GrailsDomainHasEquals.enabled = false + ThrowRuntimeException.enabled = false + CatchException.enabled = false + MisorderedStaticImports.enabled = false + EmptyMethod.doNotApplyToClassNames = '*Controller' } } diff --git a/plugins/frontlinesms-core/grails-app/conf/Config.groovy b/plugins/frontlinesms-core/grails-app/conf/Config.groovy index 930ea9d92..ad7887221 100644 --- a/plugins/frontlinesms-core/grails-app/conf/Config.groovy +++ b/plugins/frontlinesms-core/grails-app/conf/Config.groovy @@ -41,7 +41,7 @@ grails.views.gsp.encoding = "UTF-8" grails.converters.encoding = "UTF-8" // enable Sitemesh preprocessing of GSP pages grails.views.gsp.sitemesh.preprocess = true -grails.views.pagination.max = 50 +grails.views.pagination.max = 1000 // scaffolding templates configuration grails.scaffolding.templates.domainSuffix = 'Instance' @@ -62,13 +62,13 @@ grails.hibernate.cache.queries = true grails.plugin.databasemigration.updateOnStartFileNames = ['changelog.groovy'] // Allow disabling of migrations via system property +println "MIGRATIONS: System.properties.'db.migrations' = ${System.properties['db.migrations']}" grails.plugin.databasemigration.updateOnStart = System.properties['db.migrations'] != 'false' // set per-environment settings environments { development { grails.logging.jul.usebridge = true - } production { grails.logging.jul.usebridge = false @@ -95,7 +95,7 @@ log4j = { threshold:org.apache.log4j.Level.WARN } development { console name:'dev', threshold:org.apache.log4j.Level.INFO } - test { console name:'test', threshold:org.apache.log4j.Level.INFO } + test { console name:'test', threshold:org.apache.log4j.Level.ERROR } } root { @@ -123,3 +123,9 @@ jqueryValidation.cdn = false // false or "microsoft" jqueryValidation.additionalMethods = false frontlinesms.plugins=['frontlinesms-core'] +frontlinesms.blockedNotificationList = [] + +mobileNumbers.international.warn = true +mobileNumbers.nonNumeric.warn = true + +upload.maximum.size=15728640 diff --git a/plugins/frontlinesms-core/grails-app/conf/CoreBootStrap.groovy b/plugins/frontlinesms-core/grails-app/conf/CoreBootStrap.groovy index 61b81347e..7509ddbbb 100644 --- a/plugins/frontlinesms-core/grails-app/conf/CoreBootStrap.groovy +++ b/plugins/frontlinesms-core/grails-app/conf/CoreBootStrap.groovy @@ -20,16 +20,19 @@ import org.codehaus.groovy.grails.commons.ApplicationHolder class CoreBootStrap { def applicationContext + def appInfoService def appSettingsService def grailsApplication def deviceDetectionService def failPendingMessagesService + def fconnectionService def localeResolver def camelContext - def messageSource def quartzScheduler + def systemNotificationService - def bootstrapData = Environment.current == Environment.DEVELOPMENT || Boolean.parseBoolean(System.properties['frontlinesms2.bootstrap.data']?:'') + def bootstrapData = System.properties['frontlinesms2.bootstrap.data']? Boolean.parseBoolean(System.properties['frontlinesms2.bootstrap.data']): + Environment.current == Environment.DEVELOPMENT def init = { servletContext -> println "BootStrap.init() : Env=${Environment.current}" @@ -37,13 +40,16 @@ class CoreBootStrap { MetaClassModifiers.addAll() initAppSettings() + updateAvailableFconnections() if(Environment.current == Environment.TEST) { quartzScheduler.start() - test_initGeb(servletContext) dev_disableSecurityFilter() // never show new popup during tests appSettingsService['newfeatures.popup.show.immediately'] = false + //default routing in tests is to use any available connections + appSettingsService.set('routing.use', 'uselastreceiver') + appSettingsService.set('routing.preferences.edited', true) } if(Environment.current == Environment.DEVELOPMENT) { @@ -66,7 +72,8 @@ class CoreBootStrap { dev_initGroups() dev_initContacts() dev_initFconnections() - dev_initFmessages() + dev_initTextMessages() + dev_initMissedCalls() dev_initPolls() dev_initAutoreplies() dev_initAutoforwards() @@ -74,19 +81,24 @@ class CoreBootStrap { dev_initAnnouncements() dev_initSubscriptions() dev_initWebconnections() + dev_initCustomActivities() dev_initLogEntries() + setDefaultMessageRoutingPreferences() } if(Environment.current == Environment.PRODUCTION) { createWelcomeNote() updateFeaturePropertyFileValues() + setDefaultMessageRoutingPreferences() } setCustomJSONRenderers() ensureResourceDirExists() deviceDetectionService.init() failPendingMessagesService.init() + CoreAppInfoProviders.registerAll(appInfoService) activateActivities() + initialiseNonSmslibFconnections() println '\\o/ FrontlineSMS started.' } @@ -105,7 +117,7 @@ class CoreBootStrap { private def createWelcomeNote() { if(!SystemNotification.count()) { - new SystemNotification(text: messageSource.getMessage('frontlinesms.welcome', null, Locale.getDefault())).save(failOnError:true) + systemNotificationService.create(code:'frontlinesms.welcome') } } @@ -124,7 +136,6 @@ class CoreBootStrap { (1..101).each { new Contact(name:"test-${it}", mobile:"number-${it}").save(failOnError:true) - if (it % 1000 == 0) println "${it}" } [new CustomField(name: 'lake', value: 'Victoria', contact: alice), @@ -143,56 +154,63 @@ class CoreBootStrap { if(!bootstrapData) return ['Friends', 'Listeners', 'Not Cats', 'Adults'].each() { createGroup(it) } } + + private def dev_initMissedCalls() { + if(!bootstrapData) return + for(i in 1..20) { + new MissedCall(src:"+1234567$i", date: new Date() - i).save() + } + } - private def dev_initFmessages() { + private def dev_initTextMessages() { if(!bootstrapData) return - def m5 = new Fmessage(src:'Jinja', date:new Date(), text:'Look at all my friends!') + def m5 = new TextMessage(src:'Jinja', date:new Date(), text:'Look at all my friends!') for(i in 1..100) m5.addToDispatches(dst:"+12345678$i", status:DispatchStatus.SENT, dateSent:new Date()).save(failOnError:true) for(i in 101..200) m5.addToDispatches(dst:"+12345678$i", status:DispatchStatus.FAILED).save(failOnError:true) for(i in 201..300) m5.addToDispatches(dst:"+12345678$i", status:DispatchStatus.PENDING).save(failOnError:true) for(i in 1..100) { - new Fmessage(src:"123456", date:new Date(), text:"Generated SENT message: $i") + new TextMessage(src:"123456", date:new Date(), text:"Generated SENT message: $i") .addToDispatches(dst:"+12345678$i", status:DispatchStatus.SENT, dateSent:new Date()) .save(failOnError:true) } for(i in 101..200) { - new Fmessage(src:"123456", date:new Date(), text:"Generated PENDING message: $i") + new TextMessage(src:"123456", date:new Date(), text:"Generated PENDING message: $i") .addToDispatches(dst:"+12345678$i", status:DispatchStatus.PENDING) .save(failOnError:true) } for(i in 201..300) { - new Fmessage(src:"123456", date:new Date(), text:"Generated FAILED message: $i") + new TextMessage(src:"123456", date:new Date(), text:"Generated FAILED message: $i") .addToDispatches(dst:"+12345678$i", status:DispatchStatus.FAILED) .save(failOnError:true) } - new Fmessage(src:'+123987123', + new TextMessage(src:'+123987123', text:'A really long message which should be beautifully truncated so we can all see what happens in the UI when truncation is required.', inbound:true, date: new Date()).save(failOnError:true) - [new Fmessage(src:'+123456789', text:'manchester rules!', date:new Date()), - new Fmessage(src:'+198765432', text:'go manchester', date:new Date()), - new Fmessage(src:'Joe', text:'pantene is the best', date:new Date()-1), - new Fmessage(src:'Jill', text:"where's the hill?", date:createDate("2011/01/21")), - new Fmessage(src:'+254675334', text:"where's the pale?", date:createDate("2011/01/20")), - new Fmessage(src:'Humpty', text:"where're the king's men?", starred:true, date:createDate("2011/01/23"))].each() { + [new TextMessage(src:'+123456789', text:'manchester rules!', date:new Date()), + new TextMessage(src:'+198765432', text:'go manchester', date:new Date()), + new TextMessage(src:'Joe', text:'pantene is the best', date:new Date()-1), + new TextMessage(src:'Jill', text:"where's the hill?", date:createDate("2011/01/21")), + new TextMessage(src:'+254675334', text:"where's the pale?", date:createDate("2011/01/20")), + new TextMessage(src:'Humpty', text:"where're the king's men?", starred:true, date:createDate("2011/01/23"))].each() { it.inbound = true it.save(failOnError:true) } (1..101).each { - new Fmessage(src:'+198765432', text:"text-${it}", date: new Date() - it, inbound:true).save(failOnError:true) + new TextMessage(src:'+198765432', text:"text-${it}", date: new Date() - it, inbound:true).save(failOnError:true) } - def m1 = new Fmessage(src: '+3245678', date: new Date(), text: "time over?") - def m2 = new Fmessage(src: 'Johnny', date:new Date(), text: "I am in a meeting") - def m3 = new Fmessage(src: 'Sony', date:new Date(), text: "Hurry up") - def m4 = new Fmessage(src: 'Jill', date:new Date(), text: "Some cool characters: कञॠ, and more: á é í ó ú ü ñ ¿ ¡ ºª") + def m1 = new TextMessage(src: '+3245678', date: new Date(), text: "time over?") + def m2 = new TextMessage(src: 'Johnny', date:new Date(), text: "I am in a meeting") + def m3 = new TextMessage(src: 'Sony', date:new Date(), text: "Hurry up") + def m4 = new TextMessage(src: 'Jill', date:new Date(), text: "Some cool characters: कञॠ, and more: á é í ó ú ü ñ ¿ ¡ ºª") m1.addToDispatches(dst:'+123456789', status:DispatchStatus.FAILED) m1.addToDispatches(dst:'+254114533', status:DispatchStatus.SENT, dateSent:new Date()).save(failOnError: true) @@ -200,7 +218,8 @@ class CoreBootStrap { m3.addToDispatches(dst:'+254116633', status:DispatchStatus.SENT, dateSent:new Date()).save(failOnError: true) m4.addToDispatches(dst:'+254115533', status:DispatchStatus.PENDING).save(failOnError:true) - new Fmessage(src:'+33445566', text:"modem message", inbound:true, date: new Date()).save(failOnError:true, flush:true) + new TextMessage(src:'+33445566', text:"modem message", inbound:true, date: new Date()).save(failOnError:true, flush:true) + new TextMessage(src:'+33445566', text:"<0_O> marvel at the HTML & how it works!", inbound:true, date: new Date()).save(failOnError:true, flush:true) } private def dev_initFconnections() { @@ -208,21 +227,21 @@ class CoreBootStrap { new EmailFconnection(name:"mr testy's email", receiveProtocol:EmailReceiveProtocol.IMAPS, serverName:'imap.zoho.com', serverPort:993, username:'mr.testy@zoho.com', password:'mister').save(failOnError:true) new ClickatellFconnection(name:"Clickatell Mock Server", apiId:"api123", username:"boris", password:"top secret").save(failOnError:true) - new IntelliSmsFconnection(name:"IntelliSms Mock connection", send:true, username:"johnmark", password:"pass_word").save(failOnError:true) + new IntelliSmsFconnection(name:"IntelliSms Mock connection", sendEnabled:true, username:"johnmark", password:"pass_word").save(failOnError:true) } - + private def dev_initRealSmslibFconnections() { if(!bootstrapData) return - new SmslibFconnection(name:"Huawei Modem", port:'/dev/cu.HUAWEIMobile-Modem', baud:9600, pin:'1234').save(failOnError:true) + new SmslibFconnection(name:"Huawei Modem", port:'/dev/cu.HUAWEIMobile-Modem', baud:9600, pin:'1234', enabled:false).save(failOnError:true) new SmslibFconnection(name:"COM4", port:'COM4', baud:9600).save(failOnError:true) new SmslibFconnection(name:"Alex's Modem", port:'/dev/ttyUSB0', baud:9600, pin:'5602').save(failOnError:true) new SmslibFconnection(name:"MobiGater Modem", port:'/dev/ttyACM0', baud:9600, pin:'1149').save(failOnError:true) - new SmssyncFconnection(name:"SMSSync connection", secret:'secret').save(flush: true, failOnError:true) + new SmssyncFconnection(name:"SMSSync connection", secret:'secret', enabled:false).save(flush: true, failOnError:true) + new FrontlinesyncFconnection(name:"FrontlineSync connection", secret:'3469', enabled:true).save(flush: true, failOnError:true) new SmslibFconnection(name:"Geoffrey's Modem", port:'/dev/ttyUSB0', baud:9600, pin:'1149').save(failOnError:true) } - - + private def dev_initMockSmslibFconnections() { if(!bootstrapData) return new SmslibFconnection(name:"MOCK95: rejects all pins", pin:'1234', port:'MOCK95', baud:9600).save(failOnError:true) @@ -232,9 +251,7 @@ class CoreBootStrap { new SmslibFconnection(name:"MOCK99: incoming messages, and can send", port:'MOCK99', baud:9600).save(failOnError:true) new SmslibFconnection(name:"MOCK100: incoming messages for autoreplies", port:'MOCK100', baud:9600).save(failOnError:true) } - - - + private def dev_initPolls() { if(!bootstrapData) return def keyword1 = new Keyword(value: 'FOOTBALL') @@ -258,13 +275,13 @@ class CoreBootStrap { poll1.save(failOnError:true, flush:true) poll2.save(failOnError:true, flush: true) - PollResponse.findByValue('manchester').addToMessages(Fmessage.findBySrc('+198765432')) - PollResponse.findByValue('manchester').addToMessages(Fmessage.findBySrc('+123456789')) - PollResponse.findByValue('pantene').addToMessages(Fmessage.findBySrc('Joe')) + PollResponse.findByValue('manchester').addToMessages(TextMessage.findBySrc('+198765432')) + PollResponse.findByValue('manchester').addToMessages(TextMessage.findBySrc('+123456789')) + PollResponse.findByValue('pantene').addToMessages(TextMessage.findBySrc('Joe')) def barcelonaResponse = PollResponse.findByValue('barcelona'); 10.times { - def msg = new Fmessage(src: "+9198765432${it}", date: new Date() - it, text: "Yes", inbound:true); + def msg = new TextMessage(src: "+9198765432${it}", date: new Date() - it, text: "Yes", inbound:true); msg.save(failOnError: true); barcelonaResponse.addToMessages(msg); } @@ -300,63 +317,62 @@ class CoreBootStrap { ['Work', 'Projects'].each { new Folder(name:it).save(failOnError:true, flush:true) } - [new Fmessage(src:'Max', text:'I will be late'), - new Fmessage(src:'Jane', text:'Meeting at 10 am'), - new Fmessage(src:'Patrick', text:'Project has started'), - new Fmessage(src:'Zeuss', text:'Sewage blocked')].each() { + [new TextMessage(src:'Max', text:'I will be late'), + new TextMessage(src:'Jane', text:'Meeting at 10 am'), + new TextMessage(src:'Patrick', text:'Project has started'), + new TextMessage(src:'Zeuss', text:'Sewage blocked')].each() { it.inbound = true it.date = new Date() it.save(failOnError:true, flush:true) } - [Folder.findByName('Work').addToMessages(Fmessage.findBySrc('Max')), - Folder.findByName('Work').addToMessages(Fmessage.findBySrc('Jane')), - Folder.findByName('Projects').addToMessages(Fmessage.findBySrc('Zeuss')), - Folder.findByName('Projects').addToMessages(Fmessage.findBySrc('Patrick'))].each() { + [Folder.findByName('Work').addToMessages(TextMessage.findBySrc('Max')), + Folder.findByName('Work').addToMessages(TextMessage.findBySrc('Jane')), + Folder.findByName('Projects').addToMessages(TextMessage.findBySrc('Zeuss')), + Folder.findByName('Projects').addToMessages(TextMessage.findBySrc('Patrick'))].each() { it.save(failOnError:true, flush:true) } - def m = Fmessage.findByText("modem message") + def m = TextMessage.findByText("modem message") def modem = SmslibFconnection.list()[0] modem.addToMessages(m) - modem.save(failOnError:true, flush:true) - + m.connectionId = modem.id + m.save(failOnError:true, flush:true) } private def dev_initAnnouncements() { if(!bootstrapData) return - [new Fmessage(src:'Roy', text:'I will be late'), - new Fmessage(src:'Marie', text:'Meeting at 10 am'), - new Fmessage(src:'Mike', text:'Project has started')].each() { + [new TextMessage(src:'Roy', text:'I will be late'), + new TextMessage(src:'Marie', text:'Meeting at 10 am'), + new TextMessage(src:'Mike', text:'Project has started')].each() { it.inbound = true it.date = new Date() it.save(failOnError:true, flush:true) } def a1 = new Announcement(name:'Free cars!', sentMessageText:"Everyone who recieves this message will also recieve a free Subaru") def a2 = new Announcement(name:'Office Party', sentMessageText:"Office Party on Friday!") - def sent1 = new Fmessage(src:'me', inbound:false, text:"Everyone who recieves this message will also recieve a free Subaru") - def sent2 = new Fmessage(src:'me', inbound:false, text:"Office Party on Friday!") + def sent1 = new TextMessage(src:'me', inbound:false, text:"Everyone who recieves this message will also recieve a free Subaru") + def sent2 = new TextMessage(src:'me', inbound:false, text:"Office Party on Friday!") sent1.addToDispatches(dst:'+254116633', status:DispatchStatus.SENT, dateSent:new Date()).save(failOnError:true, flush:true) sent2.addToDispatches(dst:'+254116633', status:DispatchStatus.SENT, dateSent:new Date()).save(failOnError:true, flush:true) a1.addToMessages(sent1).save(failOnError:true, flush:true) a2.addToMessages(sent2).save(failOnError:true, flush:true) - [Announcement.findByName('Free cars!').addToMessages(Fmessage.findBySrc('Roy')), - Announcement.findByName('Free cars!').addToMessages(Fmessage.findBySrc('Marie')), - Announcement.findByName('Office Party').addToMessages(Fmessage.findBySrc('Mike'))].each() { + [Announcement.findByName('Free cars!').addToMessages(TextMessage.findBySrc('Roy')), + Announcement.findByName('Free cars!').addToMessages(TextMessage.findBySrc('Marie')), + Announcement.findByName('Office Party').addToMessages(TextMessage.findBySrc('Mike'))].each() { it.save(failOnError:true, flush:true) } } - private def dev_initWebconnections() { if(!bootstrapData) return - [ new Fmessage(src:'Wanyama', text:'forward me to the server'), - new Fmessage(src:'Tshabalala', text:'a text from me'), - new Fmessage(src:'June', text:'I just arrived'), - new Fmessage(src:'Otieno', text:'I am on a map!'), - new Fmessage(src:'Ekisa', text:'I too am on a map'), - new Fmessage(src:'James', text:'I just arrived')].each() { + [ new TextMessage(src:'Wanyama', text:'forward me to the server'), + new TextMessage(src:'Tshabalala', text:'a text from me'), + new TextMessage(src:'June', text:'I just arrived'), + new TextMessage(src:'Otieno', text:'I am on a map!'), + new TextMessage(src:'Ekisa', text:'I too am on a map'), + new TextMessage(src:'James', text:'I just arrived')].each() { it.inbound = true it.date = new Date() it.save(failOnError:true, flush:true) @@ -371,12 +387,12 @@ class CoreBootStrap { extCmd.addToRequestParameters(new RequestParameter(name:'sender' , value: '${message_src_number}')) extCmd.addToRequestParameters(new RequestParameter(name:'senderName' , value: '${message_src_name}')) extCmd.save(failOnError:true, flush:true) - def sent1 = new Fmessage(src:'me', inbound:false, text:"Your messages are in 'the cloud'") + def sent1 = new TextMessage(src:'me', inbound:false, text:"Your messages are in 'the cloud'") sent1.addToDispatches(dst:'+254116633', status:DispatchStatus.SENT, dateSent:new Date()).save(failOnError:true, flush:true) extCmd.addToMessages(sent1).save(failOnError:true, flush:true) - extCmd.addToMessages(Fmessage.findBySrc('Wanyama')) - extCmd.addToMessages(Fmessage.findBySrc('Tshabalala')) - extCmd.addToMessages(Fmessage.findBySrc('June')) + extCmd.addToMessages(TextMessage.findBySrc('Wanyama')) + extCmd.addToMessages(TextMessage.findBySrc('Tshabalala')) + extCmd.addToMessages(TextMessage.findBySrc('June')) extCmd.save(failOnError:true, flush:true) def extCmdPost = new GenericWebconnection(name:'POST to Server', url:"http://192.168.0.200:9091/webservice-0.1/message/post", httpMethod:Webconnection.HttpMethod.POST) .addToKeywords(value:'POST') @@ -392,16 +408,15 @@ class CoreBootStrap { ushahidiWebconnection.addToRequestParameters(new RequestParameter(name:'key' , value: '1NIJP34G')) ushahidiWebconnection.addToRequestParameters(new RequestParameter(name:'s' , value: '${message_src_number}')) ushahidiWebconnection.save(failOnError:true, flush:true) - def ushSent = new Fmessage(src:'me', inbound:false, text:"Your messages are on Ushahidi!") + def ushSent = new TextMessage(src:'me', inbound:false, text:"Your messages are on Ushahidi!") ushSent.addToDispatches(dst:'+25411663123', status:DispatchStatus.SENT, dateSent:new Date()).save(failOnError:true, flush:true) ushahidiWebconnection.addToMessages(ushSent).save(failOnError:true, flush:true) - ushahidiWebconnection.addToMessages(Fmessage.findBySrc('Otieno')) - ushahidiWebconnection.addToMessages(Fmessage.findBySrc('Ekisa')) - ushahidiWebconnection.addToMessages(Fmessage.findBySrc('James')) + ushahidiWebconnection.addToMessages(TextMessage.findBySrc('Otieno')) + ushahidiWebconnection.addToMessages(TextMessage.findBySrc('Ekisa')) + ushahidiWebconnection.addToMessages(TextMessage.findBySrc('James')) ushahidiWebconnection.save(failOnError:true, flush:true) } - private def dev_initSubscriptions() { if(!bootstrapData) return @@ -433,9 +448,35 @@ class CoreBootStrap { footballGroup.save(failOnError:true) } + + private def dev_initCustomActivities() { + if(!bootstrapData) return + + def uploadStep = new WebconnectionActionStep() + .setPropertyValue('url', 'http://frontlinesms.com') + .setPropertyValue('httpMethod', 'GET') + .setPropertyValue('myNumber', '23123123') + .setPropertyValue('myMessage', 'i will upload forever') + def joinStep = new JoinActionStep().addToStepProperties(new StepProperty(key:"group", value:"1")) + def forwardStep = new ForwardActionStep() + .addToStepProperties(new StepProperty(key:'sentMessageText',value:'sending forward ${message_text}')) + .addToStepProperties(new StepProperty(key:'recipient',value:'Address-123123')) + def leaveStep = new JoinActionStep().addToStepProperties(new StepProperty(key:"group", value:"2")) + def replyStep = new ReplyActionStep().addToStepProperties(new StepProperty(key:"autoreplyText", value:"I will send you forever")) + + new CustomActivity(name:'Do it all') + .addToSteps(joinStep) + .addToSteps(leaveStep) + .addToSteps(replyStep) + .addToSteps(forwardStep) + .addToSteps(uploadStep) + .addToKeywords(value:"CUSTOM") + .save(failOnError:true, flush:true) + } private def dev_initLogEntries() { if(!bootstrapData) return + def now = new Date() [new LogEntry(date:now, content: "entry1"), new LogEntry(date:now-2, content: "entry2"), @@ -453,21 +494,29 @@ class CoreBootStrap { def c = new Contact(name: n, mobile: a) c.save(failOnError: true) } - + private def initialiseSerial() { if(Environment.current == Environment.TEST - || Boolean.parseBoolean(System.properties['serial.mock'])) + || Boolean.parseBoolean(System.properties['serial.mock'])) { initialiseMockSerial() - else + } else { initialiseRealSerial() + } - println "PORTS:" - serial.CommPortIdentifier.portIdentifiers.each { - println "> Port identifier: ${it}" + def ports = serial.CommPortIdentifier.portIdentifiers + if(ports) { + println "PORTS:" + ports.each { + println "> Port identifier: ${it}" + } + println "END OF PORTS LIST" + } else { + println '''NO SERIAL PORTS DETECTED. IF YOU ARE RUNNING *NIX, PLEASE CHECK THAT YOU +ARE A MEMBER OF THE APPROPRIATE GROUP (e.g. "dialout"). OTHERWISE MAKE SURE THAT +YOU HAVE A COMPATIBLE SERIAL LIBRARY INSTALLED.''' } - println "END OF PORTS LIST" } - + private def initialiseRealSerial() { dev_initRealSmslibFconnections() @@ -510,6 +559,15 @@ class CoreBootStrap { serial.SerialClassFactory.init(serial.SerialClassFactory.PACKAGE_RXTX) // TODO hoepfully this step of specifying the package is unnecessary } + private def initialiseNonSmslibFconnections() { + Fconnection.findAllByEnabled(true).each { connection -> + if (connection.shortName != "smslib") { + println "CoreBootStrap.initialiseNonSmslibFconnections() :: creating routes for $connection.shortName:$connection.id" + fconnectionService.createRoutes(connection) + } + } + } + private def activateActivities() { Activity.findAllByArchivedAndDeleted(false, false).each { activity -> activity.activate() @@ -542,51 +600,6 @@ class CoreBootStrap { private def dev_initMockPortMessages() { return ["AUTOREPLY", "autorely", "auToreply", "colorz", "color z", ""]; } - - private def test_initGeb(def servletContext) { - // N.B. this setup uses undocumented features of Geb which could be subject - // to unnanounced changes in the future - take care when upgrading Geb! - def emptyMc = new geb.navigator.AttributeAccessingMetaClass(new ExpandoMetaClass(geb.navigator.EmptyNavigator)) - def nonEmptyMc = new geb.navigator.AttributeAccessingMetaClass(new ExpandoMetaClass(geb.navigator.NonEmptyNavigator)) - - final String contextPath = servletContext.contextPath - // FIXME in grails 2, serverURL appears to be not set, so hard-coding it here - //final String baseUrl = grailsApplication.config.grails.serverURL - final String serverPort = grailsApplication.config.grails.serverPort?:System.properties['server.port']?: '8080' - final String baseUrl = "http://localhost:${serverPort}/frontlinesms-core" - nonEmptyMc.'get@href' = { - def val = getAttribute('href') - if(val.startsWith(contextPath)) val = val.substring(contextPath.size()) - // check for baseUrl second, as it includes the context path - else if(val.startsWith(baseUrl)) val = val.substring(baseUrl.size()) - return val - } - - /** list of boolean vars from https://selenium.googlecode.com/svn/trunk/docs/api/java/org/openqa/selenium/WebElement.html#getAttribute(java.lang.String) */ - final def BOOLEAN_PROPERTIES = ['async', 'autofocus', 'autoplay', 'checked', 'compact', 'complete', 'controls', 'declare', 'defaultchecked', 'defaultselected', 'defer', 'disabled', 'draggable', 'ended', 'formnovalidate', 'hidden', 'indeterminate', 'iscontenteditable', 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nohref', 'noresize', 'noshade', 'novalidate', 'nowrap', 'open', 'paused', 'pubdate', 'readonly', 'required', 'reversed', 'scoped', 'seamless', 'seeking', 'selected', 'spellcheck', 'truespeed', 'willvalidate'] - BOOLEAN_PROPERTIES.each { name -> - def getterName = "is${name.capitalize()}" - emptyMc."$getterName" = { false } - nonEmptyMc."$getterName" = { - def v = delegate.getAttribute(name) - !(v == null || v.length()==0 || v.equalsIgnoreCase('false')) - } - } - - def oldMethod = nonEmptyMc.getMetaMethod("setInputValue", [Object] as Class[]) - nonEmptyMc.setInputValue = { value -> - if(input.tagName == 'selected') { - throw new RuntimeException("OMG youre playing with selecters!") - } else { - oldMethod.invoke(value) - } - } - - emptyMc.initialize() - geb.navigator.EmptyNavigator.metaClass = emptyMc - nonEmptyMc.initialize() - geb.navigator.NonEmptyNavigator.metaClass = nonEmptyMc - } private def dev_disableSecurityFilter() { appSettingsService.set("auth.basic.enabled", '') @@ -595,7 +608,7 @@ class CoreBootStrap { } private Date createDate(String dateAsString) { - DateFormat format = createDateFormat(); + DateFormat format = createDateFormat() return format.parse(dateAsString) } @@ -605,8 +618,7 @@ class CoreBootStrap { private def ensureResourceDirExists() { def dir = new File(ResourceUtils.getResourcePath()) - if (!dir.exists()) - { + if (!dir.exists()) { dir.mkdirs() log.info "creating resource directory at {$dir.absolutePath}" } @@ -614,12 +626,32 @@ class CoreBootStrap { private def setCustomJSONRenderers() { JSON.registerObjectMarshaller(Announcement) { - def returnArray = [:] - returnArray['id'] = it.id - returnArray['dateCreated'] = it.dateCreated - returnArray['name'] = it.name - returnArray['sentMessageText'] = it.sentMessageText - return returnArray - } + [id:it.id, dateCreated:it.dateCreated, name:it.name, sentMessageText:it.sentMessageText] + } + JSON.registerObjectMarshaller(DetectedDevice) { + [port:it.port, description:it.description, lockType:it.lockType] + } + } + + private setDefaultMessageRoutingPreferences(){ + if(!appSettingsService.get('routing.preferences.edited') || (appSettingsService.get('routing.preferences.edited') == false)){ + println "### Changing Routing preferences ###" + appSettingsService.set('routing.uselastreceiver', false) + appSettingsService.set('routing.preferences.edited', true) + } + else { + def fconnectionInstanceList = Fconnection.findAllBySendEnabled(true) + def fconnectionIdList = fconnectionInstanceList.collect {"fconnection-${it.id}"}.join(",") + appSettingsService.set('routing.use', fconnectionIdList) + } + } + + private def updateAvailableFconnections() { + println "# CoreBootStrap.updateAvailableFconnections() :: Fconnection implementations before pruning: ${Fconnection.implementations}" + Fconnection.implementations.remove(ClickatellFconnection) + Fconnection.implementations.remove(IntelliSmsFconnection) + Fconnection.implementations.remove(NexmoFconnection) + println "# CoreBootStrap.updateAvailableFconnections() :: Fconnection implementations after pruning: ${Fconnection.implementations}" } } + diff --git a/plugins/frontlinesms-core/grails-app/conf/CoreResources.groovy b/plugins/frontlinesms-core/grails-app/conf/CoreResources.groovy index 0ffe09eb7..68f0600a0 100644 --- a/plugins/frontlinesms-core/grails-app/conf/CoreResources.groovy +++ b/plugins/frontlinesms-core/grails-app/conf/CoreResources.groovy @@ -1,7 +1,9 @@ modules = { common { dependsOn 'frontlinesms-core' } + 'frontlinesms-core' { - dependsOn "jquery, jquery-ui" + dependsOn 'jquery-ui' + dependsOn 'flags' resource url:[dir:'css', file:'reset.css'] resource url:[dir:'css', file:'layout.css'] resource url:[dir:'css', file:'head.css'] @@ -10,71 +12,84 @@ modules = { resource url:[dir:'css', file:'contact.css'] resource url:[dir:'css', file:'archive.css'] resource url:[dir:'css', file:'activity.css'] - resource url:[dir:'css', file:"activity/webconnection.css"] + resource url:[dir:'css', file:'activity/customactivity.css'] + resource url:[dir:'css', file:'activity/webconnection.css'] resource url:[dir:'css', file:'search.css'] resource url:[dir:'css', file:'settings.css'] resource url:[dir:'css', file:'status.css'] resource url:[dir:'css', file:'wizard.css'] - + resource url:[dir:'css', file:'chosen.css'] + resource url:[dir:'css', file:'color.css'] + resource url:[dir:'css', file:'unreviewed-core.css'] resource url:[dir:'js/layout', file:'resizer.js'], disposition:'head' resource url:[dir:'css', file:'status.css'] resource url:[dir:'css', file:'help.css'] - - resource url:[dir:'js', file:"frontlinesms_core.js"], disposition:'head' - resource url:[dir:'js', file:"activity/popups.js"], disposition:'head' - resource url:[dir:'js', file:"activity/poll/poll_graph.js"], disposition:'head' + resource url:[dir:'js', file:'frontlinesms_core.js'], disposition:'head' + resource url:[dir:'js', file:'app_info.js'], disposition:'head' + resource url:[dir:'js', file:'activity/popups.js'], disposition:'head' + resource url:[dir:'js', file:"activity/custom_activity.js"], disposition:'head' + resource url:[dir:'js', file:'activity/popupCustomValidation.js'], disposition:'head' + resource url:[dir:'js', file:'activity/poll/poll.js'], disposition:'head' + resource url:[dir:'js', file:'activity/poll/poll_graph.js'], disposition:'head' + resource url:[dir:'js', file:'activity/webconnection.js'], disposition:'head' + resource url:[dir:'js', file:'activity/subscription.js'], disposition:'head' resource url:[dir:'js', file:'button.js'], disposition:'head' resource url:[dir:'js', file:'characterSMS-count.js'], disposition:'head' - resource url:[dir:'js', file:'check_li.js'], disposition:'head' - resource url:[dir:'js', file:"jquery.ui.selectmenu.js"], disposition:'head' - resource url:[dir:'js', file:"jquery.validate.min.js"], disposition:'head' - resource url:[dir:'js', file:"mediumPopup.js"], disposition:'head' - resource url:[dir:'js', file:"new_features.js"], disposition:'head' - resource url:[dir:'js', file:"pagination.js"], disposition:'head' - resource url:[dir:'js', file:"smallPopup.js"], disposition:'head' - resource url:[dir:'js', file:"status_indicator.js"], disposition:'head' - resource url:[dir:'js', file:"system_notification.js"], disposition:'head' + resource url:[dir:'js', file:'check_list.js'], disposition:'head' + resource url:[dir:'js', file:'fconnection.js'], disposition:'head' + resource url:[dir:'js', file:'routing.js'], disposition:'head' + resource url:[dir:'js', file:'jquery.ui.selectmenu.js'], disposition:'head' + resource url:[dir:'js', file:'jquery.validate.min.js'], disposition:'head' + resource url:[dir:'js', file:'mediumPopup.js'], disposition:'head' + resource url:[dir:'js', file:'new_features.js'], disposition:'head' + resource url:[dir:'js', file:'pagination.js'], disposition:'head' + resource url:[dir:'js', file:'recipient_selecter.js'], disposition:'head' + resource url:[dir:'js', file:'sanchez.min.js'], disposition:'head' + resource url:[dir:'js', file:'settings/connectionTooltips.js'], disposition:'head' + resource url:[dir:'js', file:'smallPopup.js'], disposition:'head' + resource url:[dir:'js', file:'status_indicator.js'], disposition:'head' + resource url:[dir:'js', file:'system_notification.js'], disposition:'head' resource url:[dir:'js', file:'magicwand.js'], disposition:'head' + resource url:[dir:'js', file:'contactsearch.js'], disposition:'head' + resource url:[dir:'js', file:'chosen.jquery.js'], disposition:'head' + resource url:[dir:'js', file:'ajax-chosen.js'], disposition:'head' resource url:[dir:'js', file:'selectmenuTools.js'], disposition:'head' - resource url:[dir:'js', file:"activity/popupCustomValidation.js"], disposition:'head' - - resource url:[dir:'js', file:"guiders-1.2.8.js"], disposition:'head' - resource url:[dir:'css', file:"guiders-1.2.8.css"], disposition:'head' + resource url:[dir:'js', file:'jquery.autosize-min.js'] + resource url:[dir:'js', file:'message_composer.js'] + resource url:[dir:'js', file:'frontlinesync.js', disposition:'head'] + resource url:[dir:'js', file:'inline_editable.js', disposition:'head'] } messages { - dependsOn "jquery, jquery-ui, common" - resource url:[dir:'js', file:"message/arrow_navigation.js"], disposition:'head' - resource url:[dir:'js', file:"message/star_message.js"], disposition:'head' - resource url:[dir:'js', file:"message/categorize_dropdown.js"], disposition:'head' - resource url:[dir:'js', file:"message/move_dropdown.js"], disposition:'head' - resource url:[dir:'js', file:"message/moreActions.js"], disposition:'head' + dependsOn 'common, font-awesome' + resource url:[dir:'js', file:'message/arrow_navigation.js'], disposition:'head' + resource url:[dir:'js', file:'message/star_message.js'], disposition:'head' + resource url:[dir:'js', file:'message/categorize_dropdown.js'], disposition:'head' + resource url:[dir:'js', file:'message/move_dropdown.js'], disposition:'head' + resource url:[dir:'js', file:'message/moreActions.js'], disposition:'head' + resource url:[dir:'js', file:'message/check_for_new_messages.js'] + resource url:[dir:'js', file:'message/new_message_summary.js'] + resource url:[dir:'js', file:'jquery.pulse.js'] } - - newMessagesCount { - dependsOn "jquery" - resource url:[dir:'js', file:"message/check_for_new_messages.js"] - } - + archive { - dependsOn "messages" + dependsOn 'messages' } contacts { - dependsOn "common" - resource url:[dir:'js', file:"contact/buttonStates.js"] - resource url:[dir:'js', file:"contact/moreGroupActions.js"] - resource url:[dir:'js', file:"contact/search_within_list.js"] - resource url:[dir:'js', file:"contact/show-groups.js"] - resource url:[dir:'js', file:"contact/show-fields.js"] - resource url:[dir:'js', file:"contact/validateContact.js"] + dependsOn 'common, font-awesome' + resource url:[dir:'js', file:'contact/moreGroupActions.js'] + resource url:[dir:'js', file:'contact/search_within_list.js'] + resource url:[dir:'js', file:'contact/editor.js'] + resource url:[dir:'js', file:'contact/groups-editor.js'] + resource url:[dir:'js', file:'contact/validateContact.js'] } status { - dependsOn "common" - resource url:[dir:'js', file:"datepicker.js"] + dependsOn 'common' + resource url:[dir:'js', file:'datepicker.js'] } graph { @@ -85,26 +100,52 @@ modules = { resource url:[dir:'js', file:'/graph/jqplot.pointLabels.min.js'] resource url:[dir:'js', file:'/graph/jqplot.highlighter.min.js'] resource url:[dir:'js', file:'/graph/jqplot.enhancedLegendRenderer.min.js'] - resource url:[dir:'css', file:"jquery.jqplot.css"] + resource url:[dir:'css', file:'jquery.jqplot.css'] } search { - dependsOn "messages" - resource url:[dir:'js', file:"datepicker.js"] - resource url:[dir:'js', file:"search/moreOptions.js"] - resource url:[dir:'js', file:"search/basicFilters.js"] + dependsOn 'messages' + resource url:[dir:'js', file:'datepicker.js'] + resource url:[dir:'js', file:'search/moreOptions.js'] + resource url:[dir:'js', file:'search/basicFilters.js'] } settings { - dependsOn "common" - resource url:[dir:'js', file:'/settings/basicAuthValidation.js'] - resource url:[dir:'js', file:'/settings/connectionTooltips.js'] + dependsOn 'common, font-awesome' + resource url:[dir:'js', file:'/settings/general_settings.js'] + resource url:[dir:'js', file:'simple-slider.js'] + resource url:[dir:'css', file:'simple-slider.css'] + resource url:[dir:'js', file:'jquery.pulse.js'] + resource url:[dir:'js', file:'contact/import_review.js'] } overrides { 'jquery-theme' { - resource id: 'theme', url:[dir:'jquery-ui', file:"themes/medium/jquery-ui-1.8.11.custom.css"] + dependsOn 'jquery-ui-base-imports' + resource id:'theme', url:[dir:'jquery-ui', file:'themes/medium/jquery-ui.custom.css'], bundle:'frontlinesms-core' } } + 'jquery-ui-base-imports' { + resource url:[dir:'jquery-ui/themes/medium', file:'jquery.ui.core.css'], bundle:'frontlinesms-core' + resource url:[dir:'jquery-ui/themes/medium', file:'jquery.ui.autocomplete.css'], bundle:'frontlinesms-core' + resource url:[dir:'jquery-ui/themes/medium', file:'jquery.ui.datepicker.css'], bundle:'frontlinesms-core' + resource url:[dir:'jquery-ui/themes/medium', file:'jquery.ui.dialog.css'], bundle:'frontlinesms-core' + resource url:[dir:'jquery-ui/themes/medium', file:'jquery.ui.progressbar.css'], bundle:'frontlinesms-core' + resource url:[dir:'jquery-ui/themes/medium', file:'jquery.ui.resizable.css'], bundle:'frontlinesms-core' + resource url:[dir:'jquery-ui/themes/medium', file:'jquery.ui.selectable.css'], bundle:'frontlinesms-core' + resource url:[dir:'jquery-ui/themes/medium', file:'jquery.ui.selectmenu.css'], bundle:'frontlinesms-core' + resource url:[dir:'jquery-ui/themes/medium', file:'jquery.ui.slider.css'], bundle:'frontlinesms-core' + resource url:[dir:'jquery-ui/themes/medium', file:'jquery.ui.tabs.css'], bundle:'frontlinesms-core' + } + + 'internet-explorer-css' { + resource url:[dir:'css', file:'ie7.css'], bundle:'ie7' + resource url:[dir:'css', file:'ie8.css'], bundle:'ie8' + } + + flags { + resource url:[dir:'css', file:'flags.css'] + } } + diff --git a/plugins/frontlinesms-core/grails-app/conf/CoreUrlMappings.groovy b/plugins/frontlinesms-core/grails-app/conf/CoreUrlMappings.groovy index d170aaad9..3d5c9613b 100644 --- a/plugins/frontlinesms-core/grails-app/conf/CoreUrlMappings.groovy +++ b/plugins/frontlinesms-core/grails-app/conf/CoreUrlMappings.groovy @@ -9,20 +9,26 @@ class CoreUrlMappings { action = 'show' } - "/search/no_search/$messageId?"(controller:'search', action:'no_search') {} + "/search/no_search/$interactionId?"(controller:'search', action:'no_search') {} "/search/result/show/"(controller:'search', action:'result') {} - "/search/result/show/$messageId"(controller:'search', action:'result') {} + "/search/result/show/$interactionId"(controller:'search', action:'result') {} - "/message/inbox/show/$messageId"(controller:'message', action:'inbox') {} - "/message/sent/show/$messageId"(controller:'message', action:'sent') {} - "/message/pending/show/$messageId"(controller:'message', action: 'pending') {} + "/message/inbox/"(controller:'message', action:'inbox') {} + "/message/inbox/show/$interactionId"(controller:'message', action:'inbox') {} + + "/missedCall/inbox/"(controller:'missedCall', action:'inbox') {} + "/missedCall/inbox/show/$interactionId"(controller:'missedCall', action:'inbox') {} + + "/message/sent/show/$interactionId"(controller:'message', action:'sent') {} + "/message/pending/show/$interactionId"(controller:'message', action: 'pending') {} "/message/trash/show/$id"(controller:'message', action: 'trash') {} "/message/activity/$ownerId"(controller:'message', action:'activity') {} - "/message/activity/$ownerId/show/$messageId"(controller:'message', action:'activity') {} + "/message/activity/$ownerId/step/$stepId"(controller:'message', action:'activity') {} + "/message/activity/$ownerId/show/$interactionId"(controller:'message', action:'activity') {} "/message/folder/$ownerId"(controller:'message', action:'folder') {} - "/message/folder/$ownerId/show/$messageId"(controller:'message', action:'folder') {} + "/message/folder/$ownerId/show/$interactionId"(controller:'message', action:'folder') {} // Don't know why this is neccessary, but it is "/poll/create"(controller:'poll', action:'create') @@ -34,20 +40,29 @@ class CoreUrlMappings { "/autoreply/create"(controller:'autoreply', action: 'create') "/autoreply/save"(controller:'autoreply', action: 'save') "/autoforward/create"(controller:'autoforward', action: 'create') + + "/customactivity/create"(controller:'customactivity', action: 'create') + "/customactivity/save"(controller:'customactivity', action: 'save') - "/archive/inbox/show/$messageId"(controller:'archive', action:'inbox') {} - "/archive/sent/show/$messageId"(controller:'archive', action:'sent') {} + "/archive/inbox/show/$interactionId"(controller:'archive', action:'inbox') {} + "/archive/sent/show/$interactionId"(controller:'archive', action:'sent') {} "/archive/activity"(controller:'archive', action:'activityList') {} "/archive/activity/$ownerId"(controller:'archive', action:'activity') {} - "/archive/activity/$ownerId/show/$messageId"(controller:'archive', action:'activity') {} + "/archive/activity/$ownerId/show/$interactionId"(controller:'archive', action:'activity') {} "/archive/folder"(controller:'archive', action:'folderList') {} "/archive/folder/$ownerId"(controller:'archive', action:'folder') {} - "/archive/folder/$ownerId/show/$messageId"(controller:'archive', action:'folder') {} + "/archive/folder/$ownerId/show/$interactionId"(controller:'archive', action:'folder') {} "/webconnection/$imp/$action"(controller:'webconnection') {} + "/images/help/$imagePath**.png"(controller:'help', action:'image') {} + "/help/$helpSection**"(controller:'help', action:'section') {} + "/help"(controller:'help', action:'index') {} + + "/api/1/$entityClassApiUrl/$entityId/$secret?" controller:'api' + "/$controller/$action?/$id?"{ constraints { // apply constraints here diff --git a/plugins/frontlinesms-core/grails-app/conf/DataSource.groovy b/plugins/frontlinesms-core/grails-app/conf/DataSource.groovy index 0d4f56c1d..50cb4735e 100644 --- a/plugins/frontlinesms-core/grails-app/conf/DataSource.groovy +++ b/plugins/frontlinesms-core/grails-app/conf/DataSource.groovy @@ -11,38 +11,42 @@ hibernate { } // environment specific settings environments { - development { - dataSource { - def runMigrations = System.properties.'db.migrations' - if(runMigrations == "false") { - println "WARNING:: DATABASE MIGRATION DISABLED" - dbCreate = "create-drop" - } - url = "jdbc:h2:mem:devDb;MVCC=TRUE" - } - } - test { - dataSource { - dbCreate = "update" - url = "jdbc:h2:mem:testDb${frontlinesms2.StaticApplicationInstance.uniqueId};MVCC=TRUE" - logSql = true - } - } - production { - dataSource { - def prodDbName = System.properties.'db.name' ?: 'prodDb' // production DB name defaults to prodDb - url = "jdbc:h2:${frontlinesms2.ResourceUtils.resourcePath}/${prodDbName};MVCC=TRUE" - pooled = true - properties { - maxActive = -1 - minEvictableIdleTimeMillis=1800000 - timeBetweenEvictionRunsMillis=1800000 - numTestsPerEvictionRun=3 - testOnBorrow=true - testWhileIdle=true - testOnReturn=true - validationQuery="SELECT 1" - } - } - } + def runMigrations = System.properties.'db.migrations' != 'false' + development { + dataSource { + if(!runMigrations) { + println "WARNING:: DATABASE MIGRATION DISABLED" + dbCreate = 'create-drop' + } + url = "jdbc:h2:mem:devDb;MVCC=TRUE" + } + } + test { + dataSource { + dbCreate = 'update' + url = "jdbc:h2:mem:testDb${frontlinesms2.StaticApplicationInstance.uniqueId};MVCC=TRUE" + } + } + production { + dataSource { + if(!runMigrations) { + println "WARNING:: DATABASE MIGRATION DISABLED" + dbCreate = 'create' + } + def prodDbName = System.properties.'db.name' ?: 'prodDb' // production DB name defaults to prodDb + url = "jdbc:h2:${frontlinesms2.ResourceUtils.resourcePath}/${prodDbName};MVCC=TRUE" + pooled = true + properties { + maxActive = -1 + minEvictableIdleTimeMillis=1800000 + timeBetweenEvictionRunsMillis=1800000 + numTestsPerEvictionRun=3 + testOnBorrow=true + testWhileIdle=true + testOnReturn=true + validationQuery="SELECT 1" + } + } + } } + diff --git a/plugins/frontlinesms-core/grails-app/conf/TestDataConfig.groovy b/plugins/frontlinesms-core/grails-app/conf/TestDataConfig.groovy index 951adb93d..6b4105f62 100644 --- a/plugins/frontlinesms-core/grails-app/conf/TestDataConfig.groovy +++ b/plugins/frontlinesms-core/grails-app/conf/TestDataConfig.groovy @@ -3,7 +3,7 @@ import frontlinesms2.* testDataConfig { def counter = 0 sampleData { - 'frontlinesms2.Fmessage' { + 'frontlinesms2.TextMessage' { inbound = true src = '+254701234567' text = 'build-test-data made me' @@ -13,10 +13,10 @@ testDataConfig { messages = [] } 'frontlinesms2.IntelliSmsFconnection' { - send = true + sendEnabled = true username = 'uname' password = 'secret' - receive = true + receiveEnabled = true receiveProtocol = frontlinesms2.EmailReceiveProtocol.POP3 serverName = 'example.com' serverPort = 1234 diff --git a/plugins/frontlinesms-core/web-app/help/core/activities/1.getting_around_activities.txt b/plugins/frontlinesms-core/grails-app/conf/help/activities/1.getting_around_activities.txt similarity index 57% rename from plugins/frontlinesms-core/web-app/help/core/activities/1.getting_around_activities.txt rename to plugins/frontlinesms-core/grails-app/conf/help/activities/1.getting_around_activities.txt index 2e4572854..951efbcff 100644 --- a/plugins/frontlinesms-core/web-app/help/core/activities/1.getting_around_activities.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/activities/1.getting_around_activities.txt @@ -1,14 +1,14 @@ # Getting Around Activities -You can access activities from the messages tab. When you click on an activity for the first time, you will notice that it has a slightly different layout in comparison to the [message inbox][1]. This is because an activity performs a specialised function such as [Polling][2], [Announcements][3] or an [Auto Reply][5]. More detailed information about each kind of activity can be found on the respective activities help page. The unique interface for an activity has been designed so that all the information can be accessed and managed easier. +You can access activities from the messages tab. When you click on an activity for the first time, you will notice that it has a slightly different layout in comparison to the [message inbox][1]. This is because an activity performs a specialized function such as [Polling][2], [Announcements][3] or an [Auto Reply][5]. More detailed information about each kind of activity can be found on the respective activity's help page. The unique interface for an activity has been designed so that all the information can be accessed and managed more easily. ![activity overciew][17] ### Activity Header -This large header (1) contains summary information about the Activity. Basic information such as the name of the activity, the date it was created, the settings it was created with and how many messages were sent as part of the activity. -So, in the example of a Poll, you would also find here the question sent as part of the poll, and whether there was an Auto Reply set up. If an Auto Reply was set up, the message used in the [auto-reply][5] will also be displayed here. -Further, a breakdown of Poll results will be displayed here as well. +This large header (1) contains summary information about the Activity, such as the name of the activity, the date it was created, the settings it was created with and how many messages were sent as part of the activity. + +So, in the example of a Poll, you would also find here the question sent as part of the poll, and whether an Auto Reply was set up. If an Auto Reply was set up, the message used in the [auto-reply][5] will also be displayed here. A breakdown of Poll results will also be displayed. The [Quick Message][6], [Archive][7], [Export][8], [Rename][9] and [Delete][10] actions can be accessed from this header as well. @@ -32,27 +32,27 @@ The message detail area (3) contains the full text of the selected message and s You can [reply][15], [move][15], [forward][15] and [delete][15] messages from here. -['Categorise Response'][13] is a Poll specific function and will allow you to change the response under which the message is categorised. +['Categorise Response'][13] is specific to Polls and will allow you to change the response under which the message is categorized. ### Related Actions [Getting Around the Inbox][16] [Creating a Poll][2] -[Manually Categorising a Response][13] +[Manually Categorizing a Response][13] [Creating an Announcement][3] [Creating an Auto Reply][5] -[1]: core/messages/1.getting_around_the_messages_tab -[2]: core/activities/3.creating_a_poll -[3]: core/activities/4.creating_an_announcement -[5]: core/activities/5.creating_an_auto-reply -[6]: core/messages/3.quick_message -[7]: core/archive/1c.activity_archive -[8]: core/messages/9.exporting -[9]: core/activities/6.renaming_an_activity -[10]: core/messages/8.mrfd -[11]: core/activities/3.creating_a_poll -[13]: core/activities/3a.manually_categorising -[15]: core/messages/8.mrfd -[16]: core/messages/1.getting_around_the_messages_tab +[1]: ../messages/1.getting_around_the_messages_tab +[2]: ../activities/3.creating_a_poll +[3]: ../activities/4.creating_an_announcement +[5]: ../activities/5.creating_an_auto-reply +[6]: ../messages/3.quick_message +[7]: ../archive/1c.activity_archive +[8]: ../messages/9.exporting +[9]: ../activities/6.renaming_an_activity +[10]: ../messages/8.mrfd +[11]: ../activities/3.creating_a_poll +[13]: ../activities/3a.manually_categorising +[15]: ../messages/8.mrfd +[16]: ../messages/1.getting_around_the_messages_tab [17]: ../images/help/activity_overview.png [18]: ../images/help/poll_chart.png diff --git a/plugins/frontlinesms-core/grails-app/conf/help/activities/10.web_connection_api.txt b/plugins/frontlinesms-core/grails-app/conf/help/activities/10.web_connection_api.txt new file mode 100644 index 000000000..0cc4b6bf7 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/conf/help/activities/10.web_connection_api.txt @@ -0,0 +1,77 @@ +# The FrontlineSMS Web Connection API + +FrontlineSMS lets you set up Web Connections to external web services. The primary function of Web Connections is to trigger HTTP requests on the external service when messages are received. However, in addition to this, you can enable the FrontlineSMS API through a Web Connection Activity, which allows your external web service to trigger outgoing messages through FrontlineSMS. + +## Enabling the API + +In the Web Connection walkthrough, you will be given the option to enable the FrontlineSMS API for that Activity. The only required configuration is the API Key that the remote service will need to use in each request. See [Creating a Web Connection][] for details on setting up a Web Connection. + +## API Details + +The URL for a specific Activity's API endpoint will be shown in in the activity's header. + +The Web Connection API expects a JSON Map with the following parameters: +- secret +- message +- recipients + +### secret (required if API Secret is set in Web Connection instance) +When configuring a Web Connection, you will be prompted to enter an API Secret. Incoming requests will only be processed if the secret parameter matches what was set up in the Web Connection walkthrough. If no secret was provided, the secret parameter can be left out of a request. + +### message (required) +This is the message body that will be sent out as the SMS. Substitution variables of the type used within FrontlineSMS are supported (so, for example, ${contact_name} will be replaced with the receiving contact's name). As with any SMS, extra cost may be incurred if this message body is longer than 160 characters. + +### recipients (required) +This is a list of entities the message should be sent to. Each entity will have a 'type' parameter and a corresponding value parameter, depending on the type. The available recipient entities are listed below. + +#### group + +The value parameter here can either be a 'name', which takes a literal string representing the group name, or 'id', which takes the FrontlineSMS database id of the group. + +#### smartgroup + +The value parameter here can either be a 'name', which takes a literal string representing the smartgroup name, or 'id', which takes the FrontlineSMS database id of the smartgroup. + +#### contact + +The value parameter here can either be a 'name', which takes a literal string representing the contact name, or 'id', which takes the FrontlineSMS database id of the contact. + +#### address + +This represents a phone number. It must be accompanied by a 'value' attribute, which represents the phone number to send to. Phone numbers should be parsed in international format without spaces or brackets. + + +Below is an example of a valid requestst body, utilising all the valid combinations of recipient types. + + URL: [api/webconnection/${activityId}/${secret}]/send + METHOD: POST + FORMAT: JSON + CONTENT: { + "secret":"secret", + "message":"Hello, ${contact_name}", + "recipients":[ + { "type":"group", "id":"1" }, + { "type":"group", "name":"friends" }, + { "type":"smartgroup", "id":"3" }, + { "type":"smartgroup", "name":"humans" }, + { "type":"contact", "id":"2" }, + { "type":"contact", "name":"bobby" }, + { "type":"address", "value":"+1234567890" } + ] + } + +## Responses + +FrontlineSMS will return various HTTP response codes depending on the outcome of the request. + +### 200 (Success) + +This will be accompanied by a text body stating how many recipients received the message. Note that this only signifies successful submission of the request to FrontlineSMS, and does not necessarily mean the outgoing message will be successfully delivered. If there are no routes available, or a problem with any of the active routes, the outgoing messages may fail despite the 200 response + +### 400 (Bad Request) + +This will be accompanied by an error message stating what was missing in the request. Examples are "no recipients supplied" and "missing required field(s): message". Note that if you specify valid group or smartgroup names, you may still get a 400 if there are currently no members of those groups + +### 401 (Authentication required) + +This error code will occur when the Web Connection requires a 'secret' parameter and none was supplied, or the secret provided was incorrect. The body will specify which of these two conditions occurred, as it will either be "no secret provided" or "invalid secret" diff --git a/plugins/frontlinesms-core/grails-app/conf/help/activities/11.custom_activity_builder b/plugins/frontlinesms-core/grails-app/conf/help/activities/11.custom_activity_builder new file mode 100644 index 000000000..8375d8d76 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/conf/help/activities/11.custom_activity_builder @@ -0,0 +1,82 @@ +# Using the Custom Activity Builder + +FrontlineSMS offers a number of ways to create rules to help sort your incoming messages. One of the most powerful tools is the Custom Activity Builder, which lets you trigger multiple actions once a message is received, such as moving the sender to a different group, replying to the message, and/or forwarding the message. You can do any or all of these activities at the same time. + +The possibilities for the Custom Activity Builder are nearly limitless. In this help file, we'll show you how to navigate the Custom Activity Builder step-by-step, and walk through a simple example to show you how it works. + +1. In your inbox, select 'Create New Activity' on the left-hand side of the screen. Then select Custom Activity Builder from the set of options in the list. We've highlighted the important components in the screenshot below: + +![Create Custom Activity][1] + +2. Determine whether or not you want to use a 'Keyword' to help automatically sort incoming messages. + +A keyword is a word that tells FrontlineSMS to take a specific action with your messages, such as adding the sender to a group, or filing the message under the activity. For instance, if you were asking people for their favorite color, the keyword might be "Color." If someone responds with "Color blue", then their response will be sorted as per your instructions, rather than simply appearing in the inbox. Alternatively, you can choose to have no keyword. + +**Note: selecting 'Do Not Use A Keyword' means that any messages coming into the system will be associated with your activity -- unless they have a keyword you've associated with some other activity. In other words, any messages without a keyword will be sorted here. Use caution when selecting this option, as unintended messages may wind up being sorted here.** + +If you choose "Do not automatically sort messages," then you will need to manually add messages to your activity at a later point. + +If you want a keyword, enter the word of your choice, then click 'next.' + +Here is a screenshot in which we've assigned the keyword, "color". + +![Custom Activity Builder Keyword][2] + +3. Now determine what you want your messages to do. + +This is where the Custom Activity Builder gets exciting, because you can make messages trigger any number of actions. + +For instance, let's assume we have a group of people with "no color preference." But someone now sends a message with the phrase, "Color blue." We now know that person has a favorite color! + +First, we can automatically add them to a group that says "Specific color preference." You can select "add sender to group" as indicated in the screenshot. + +![Custom Activity Builder Group][3] + +Now you will be given the choice on which group the sender should join. + +Note: you will need to have already created a group in advance for this to work. + +In our example, we'll add someone to the group "Specific color preference" + +![Custom Activity Builder Add To Group][4] + +Now, we might also want to remove them from another group, in this case "No color preference." + +So, we can add a step by selecting another action from the drop-down menu, as indicated in the screenshot. + +![Custom Activity Builder Remove From Group][5] + +Now you can see the multiple actions in the screenshot below. If you change your mind and want to remove an action, you can simply click the 'x' on the right-hand side of the action. + +If you're satisfied with your plan, then click 'Next.' + +![Custom Activity Builder Remove From Group][6] + + Step 5: Name your activity, then save. + +Finally, choose a name for your activity and save. Once you've done this, you're finished! Messages will behave as instructed. + +![Custom Activity Builder Remove From Group][7] + +### Related Actions +[Creating an Auto-Reply][8] +[Creating a group][9] +[Creating a smart group][10] +[Getting Around Activities][11] + +[1]: ../images/help/CustomActivityBuilder/CreateCustomActivity.png +[2]: ../images/help/CustomActivityBuilder/CABKeyword.png +[3]: ../images/help/CustomActivityBuilder/CABAddStep.png +[4]: ../images/help/CustomActivityBuilder/CABSelectGroup.png +[5]: ../images/help/CustomActivityBuilder/CABRemoveFromGroup.png +[6]: ../images/help/CustomActivityBuilder/EditingCAB.png +[7]: ../images/help/CustomActivityBuilder/CABSave.png +[8]: ../activities/5.creating_an_auto-reply +[9]: ../contacts/4.creating_a_group +[10]: ../contacts/5.creating_a_smart_group +[11]: ../activities/1.getting_around_activities + + + + + diff --git a/plugins/frontlinesms-core/grails-app/conf/help/activities/11.custom_activity_builder.txt b/plugins/frontlinesms-core/grails-app/conf/help/activities/11.custom_activity_builder.txt new file mode 100644 index 000000000..8f4ca5cf9 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/conf/help/activities/11.custom_activity_builder.txt @@ -0,0 +1,79 @@ +# Using the Custom Activity Builder + +FrontlineSMS offers a number of ways to create rules to help sort your incoming messages. One of the most powerful tools is the Custom Activity Builder, which lets you trigger multiple actions once a message is received, such as moving the sender to a different group, replying to the message, and/or forwarding the message. You can do any or all of these activities at the same time. + +The possibilities for the Custom Activity Builder are nearly limitless. In this help file, we'll show you how to navigate the Custom Activity Builder step-by-step, and walk through a simple example to show you how it works. + +1. In your inbox, select 'Create New Activity' on the left-hand side of the screen. + +You can see the button in the screenshot below: + +![Create Custom Activity][1] + +2. Now select Custom Activity Builder from the set of options in the list. + +We've circled the option in the screenshot above. + +3. Determine whether or not you want to use a 'Keyword' to help automatically sort incoming messages. + +A keyword is a word that tells FrontlineSMS to take a specific action with your messages, such as adding the sender to a group, or filing the message under the activity. For instance, if you were asking people for their favorite color, the keyword might be "Color." If someone responds with "Color blue," then their response will be sorted as per your instructions, rather than simply appearing in the inbox. Alternatively, you can choose to have no keyword. + +*Note: selecting 'Do Not Use A Keyword' means that any messages coming into the system will be associated with your activity -- unless they have a keyword you've associated with some other activity. In other words, any messages without a keyword will be sorted here. Use caution when selecting this option, as unintended messages may wind up being sorted here.* + +If you choose "Do not automatically sort messages," then you will need to manually send messages to the activity at a later point. + +If you want a keyword, enter the word of your choice, then click 'next.' + +Here is a screenshot in which we've assigned the keyword, "Color." + +![Custom Activity Keyword][2] + +4. Determine how you want the messages to behave. + +This is where the Custom Activity Builder gets exciting, because you can make messages trigger any number of actions. + +For instance, let's assume we have a group of people with "no color preference." But someone now sends a message with the phrase, "Color blue." We now know that person has a favorite color! + +First, we can automatically add them to a group that says "Specific color preference." You can select "add sender to group" -- in our case the group is "Specific color preference" -- as indicated in the screenshot. + +![Custom Activity Dropdown][3] + +Now you will be given the choice on which group the sender should join. + +![Custom Activity Group Add][4] + +*Note: you will need to have already created a group in advance for this to work.* + +In our example, we'll add someone to the group + +Now, we might also want to remove them from another group, in this case "No color preference." + +So, we can add a step by selecting another action from the drop-down menu, as indicated in the screenshot. + +![Custom Activity Group Remove][6] + +Now you can see the list of your actions in the screenshot below. If you change your mind and want to remove an action, you can simply click the 'x' on the right-hand side of the action. + +If you're satisfied with your plan, then click 'Next.' + +![Editing Custom Activity][7] + +5. Name your activity, then save. + +Finally, choose a name for your activity and save. Once you've done this, you're finished! Messages will behave as instructed. + +![Custom Activity Save][8] + +[1]: ../images/help/CustomActivityBuilder/CreateCustomActivity.png +[2]: ../images/help/CustomActivityBuilder/CABKeyword.png +[3]: ../images/help/CustomActivityBuilder/CABAddStep.png +[4]: ../images/help/CustomActivityBuilder/CABSelectGroup.png +[5]: ../images/help/CustomActivityBuilder/CABRemoveFromGroup.png +[6]: ../images/help/CustomActivityBuilder/CABRemoveFromGroup.png +[7]: ../images/help/CustomActivityBuilder/EditingCAB.png +[8]: ../images/help/CustomActivityBuilder/CABSave.png + + + + + diff --git a/plugins/frontlinesms-core/grails-app/conf/help/activities/2.creating_an_activity.txt b/plugins/frontlinesms-core/grails-app/conf/help/activities/2.creating_an_activity.txt new file mode 100644 index 000000000..594895a75 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/conf/help/activities/2.creating_an_activity.txt @@ -0,0 +1,28 @@ +# Creating an Activity + +There are several different types of Activity available to you in FrontlineSMS. Each is designed to help you use FrontlineSMS in more sophisticated ways to automatically process and visualize SMS data. Activities help you manage the way messages enter the system and the way they leave the system. For example, Announcements allow you to collect all the responses to a particular message in one folder - making them useful to create lists of attendees. Polls allow you to analyse responses to a particular question through a graphical representation of the results. You will find more detailed information on the help page of the activity you want to use. + +![Creating an activity][5] + +### [Creating a New Activity][1] + +1. Click on the "Messages" tab if you are not already viewing it. +2. Under the heading "Activity" click the button "Create New Activity". +3. A new window will appear asking you to choose the kind of activity you want to create. Choose the type you wish to create and click next. + +There are currently six kinds of activities: +[Announcements][3] +[Auto Replies][4] +[Auto Forwards][6] +[Polls][2] +[Subscriptions][7] +[Webconnections][8] + +[1]: ../activities/2.creating_an_activity +[2]: ../activities/3.creating_a_poll +[3]: ../activities/2.creating_an_announcement +[4]: ../activities/5.creating_an_auto-reply +[6]: ../activities/8.creating_an_autoforward +[7]: ../activities/??? +[8]: ../activities/??? +[5]: ../images/help/creating_activity.png diff --git a/plugins/frontlinesms-core/web-app/help/core/activities/3.creating_a_poll.txt b/plugins/frontlinesms-core/grails-app/conf/help/activities/3.creating_a_poll.txt similarity index 50% rename from plugins/frontlinesms-core/web-app/help/core/activities/3.creating_a_poll.txt rename to plugins/frontlinesms-core/grails-app/conf/help/activities/3.creating_a_poll.txt index 2384999c8..0739e1a2a 100644 --- a/plugins/frontlinesms-core/web-app/help/core/activities/3.creating_a_poll.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/activities/3.creating_a_poll.txt @@ -1,29 +1,30 @@ # Creating a Poll -A poll is a type of activity which allows you to see a graphical representation of the responses to a question. When creating a poll you will be able to enter the question and select who to send the question to. You will also be able to set up a **keyword**. A **keyword** allows FrontlineSMS to recognise which incoming messages are for polls and which aren't. It will also help the system determine which poll the message belongs to. This gives you the option to let FrontlineSMS automatically sort incoming messages into polls and you will be notified when this has happened. There are two types of responses you can set: Yes/No or a Multiple Choice style answer. You can also set up an **automatic reply** that is sent to every message received for that poll. This makes polls useful for contexts where you want to see the response to a particular issue or question._ -### How to Create a Poll +A poll is a type of activity which allows you to see a graphical representation of the responses to a question. When creating a poll you will be able to enter the question and select who to send the question to. You will also be able to set up a **keyword**. A **keyword** allows FrontlineSMS to recognize which incoming messages are for polls and which aren't. It will also help the system determine which poll the message belongs to. This gives you the option to let FrontlineSMS automatically sort incoming messages into polls - you will be notified when this has happened. There are two types of responses you can set: Yes/No or a Multiple Choice style answer. You can also set up an **automatic reply** that is sent to every message received for that poll. This makes polls useful for contexts where you want to see the response to a particular issue or question. + +## How to Create a Poll 1. From the messages tab click on 'Create New Activity'. 2. A new window will appear and you will be presented with a number of options. Choose "Poll" and click next ![Activity Select][8] -**_Enter Question_** +### Enter Question After clicking next, you will be prompted to select a question type and enter a question: ![Enter Question][9] -**Question Type:** +#### Question Type: Yes or No: Select this if your question has a Yes or No answer. For example, the question "Do you like the colour Red?", could have a Yes or No Answer. -Multiple Choice Question: Select this if your question has a number of different answers. For example, with the question "Do you like the colour Red?", you could get more detailed responses by having a range of answers such as, "Yes", "Quite a lot", "Not a lot" or "No". -If you select this option you will have the opportunity to input the possible responses in the following screens. -**Enter Question:** -Here you will find an area for you to enter your question. You can re-size the text entry box by clicking and dragging the bottom right hand corner of it. +Multiple Choice Question: Select this if your question has a number of different answers. For example, with the question "Do you like the colour Red?", you could get more detailed responses by having a range of answers such as, "Yes", "Quite a lot", "Not a lot" or "No". If you select this option you will have the opportunity to input the possible responses in the following screens. + +#### Enter Question: +Here you will find an area for you to enter your question. You can re-size the text entry box by clicking and dragging the bottom right-hand corner of it. -If you simply want to collect responses and not send out a question then check the "Do not send a message for this poll (Collect responses only)" box. This might be useful when you have already distributed the question, for example over the radio or at a meeting. +If you simply want to collect responses and not send out a question then check "Do not send a message for this poll (Collect responses only)". This might be useful when you have already distributed the question, for example over the radio or at a meeting. -**_Response List_** +### Response List If you selected the "Multiple Choice Question" then the next screen you see will allow you to specify the possible responses to the question. You can enter a maximum of 5 and a must enter a minimum of 2. ![Response List][10] @@ -32,38 +33,26 @@ Boxes A-C will be immediately available to you to edit. If you need more than 3 Once you have entered all your choices, click next to set up Automatic Sorting. -**_Automatic Sorting_** -You will be able to specify a keyword, that when included in an incoming message, will allow the system to automatically sort it into the relevant Poll. +### Automatic Sorting +First, you can choose whether or not FrontlineSMS will process incoming SMS automatically using a keyword for this activity. If you choose not to, then only messages you manually move into this activity will be processed. To sort using a keyword, select 'sort responses automatically'. -![Automatic Sorting][11] +![Automatic Sorting][17] -_**Note:** Keywords are **unique.** Once a keyword has been set, it cannot be used in a new activity even if the original activity has been archived. To re-use a keyword you have to delete the activity that keyword is associated with._ - -To set a keyword, select "Sort messages automatically that have the following Keyword:" and then enter your keyword. - -When you set a keyword, and have selected a Multiple Choice Poll type, it will be possible for you to set "Aliases" in the next step. - -Click 'next' to set Aliases if you have chosen a keyword, or the set up and Automatic Reply if you haven't. - -**_Aliases_** - -Aliases are alternate ways of spelling and/or writing the choices you entered in the "Response List". - -_This step is only available if you have selected a keyword in the previous step. If you have not, then please go to the next step._ +_**Note:** Top Level Keywords are **unique.** Once a top level keyword has been used in an activity it cannot be used in another activity, unless that activity is archived or deleted. In polls, top-level responses are optional. If you decide to allow respondents to text in only one word to vote, i.e. not to provide a top-level keyword, then the response keywords are treated as top-level and must be unique across all the activity top level keywords. -For example, if one of the choices was "Yes", you can enter alternative ways of spelling the word like "Yeah" or "Yeh". This could help in increasing the accuracy of your Poll results and reducing the amount of work you may have to do in correcting and manually categorising incorrectly spelt choices. +To set the keywords, select "Sort responses automatically.' and then set the keywords in the text boxes below. -The alias screen looks like this: +For any keyword, including each response, you can enter any number of keywords separated by commas. With this setting, any incoming message that starts with one of those keywords will trigger the activity. This means that you can cater for spelling errors, different languages, and other reasons for differing keywords. -![Aliases][17] +Default keywords will be pre-entered, but can be edited here. -Each choice is followed by an area where you can enter text. You will see two aliases already entered into the text area. These are the default settings. +![Keywords][17] -You can add more aliases simple by adding to the string of text already in the text area. Separate each alias with a comma. An example would be: +An example would be: "Yes, A, yeah, yeh" -Furthermore, the first entry in the text area corresponds to the text that will be used when generating a message in the "Edit Message" step. To change how each choice appears in the message that will be send out, change the first entry in the text area. +The first response keyword will be the one used in generating a sample message in the "Edit Message" step. To change how each choice appears in the message that will be sent out, change the first entry in the text area. If the text area looks like this for example: @@ -75,11 +64,13 @@ Once you have completed this section click "Next" to set up an "Automatic Reply" **_Automatic Reply_** At this screen you can choose to enter a message that will be automatically sent to the senders of messages identified as poll responses. This can only be selected if you have chosen to use a keyword with your poll. Without a keyword the system cannot recognise which messages to automatically reply to. + The automatic reply option will be unavailable until you create a keyword for your poll. ![Automatic Reply][12] -To enable an automatic reply, check the "Send an automatic reply to poll responses" box and enter your message in the message. +To enable an automatic reply, check the "Send an automatic reply to poll responses" box and enter your message in the text box. + This message will be sent out to everyone who sends a message with the keyword you set up in the previous step. **_Note:** This is different from the [auto-reply activity][3]._ @@ -89,21 +80,7 @@ This screen will provide you with an opportunity to view the message that will b ![Edit Message][13] -The message will be compiled from the options you have selected in previous steps. - -If you selected "yes or no" as the question type and created a keyword the default message will look like this: ->"[Your Question]? Reply "[Keyword] A" for yes, "[Keyword] B" for no." - -If you did not create a keyword then the message will look like this: ->"[Your Question]? Please reply 'Yes' or 'No'" - -If you selected "Multiple Choice Question" as question type and created a keyword, the default message will look like this: ->"[Your Question]? Reply "[Keyword] A" for [Multiple Choice A], "[Keyword] B" for [Multiple Choice B], "[Keyword] C" for [Multiple >Choice C], "[Keyword] D" for [Multiple Choice D], "[Keyword] E" for [Multiple Choice E]." - -If you did not create a keyword then the message will look like this: ->"[Your Question]? Please reply '[Multiple Choice A]' or '[Multiple Choice B]' or '[Multiple Choice C]' or '[Multiple Choice D]' or >'[Multiple Choice E]' - -Once you are happy with the message, please click 'Next'. +The message will be compiled from the options you have selected in previous steps. Once you are happy with the message, please click 'Next'. **_Select Recipients_** On this screen you will be able to either add the numbers of the people you want to send the poll to, or select them from your contacts list. @@ -111,10 +88,9 @@ On this screen you will be able to either add the numbers of the people you want ![Select Recipients][14] If you add a number manually it will be added to the list and automatically selected. This number will only remain in the system for as long as it needs to send the message and will not be added to your contacts. -Selecting groups and contacts is easy, simply select the contacts or groups you want to send the poll. **_Summary Screen_** -On this screen you will find a summary of all the options you have selected, so you can confirm that what you have created is what you would like to send. +On this screen you will find a summary of all the options you have selected, the message you've created (if applicable) and the recipients you've chosen. ![Summary][15] @@ -137,13 +113,13 @@ Your poll will be displayed under the "Activities" sub heading in the [messages [Creating a group][5] [Creating a smart group][6] -[1]: core/activities/3.creating_a_poll -[3]: core/activities/5.creating_an_auto-reply -[4]: core/activities/4.creating_an_announcement -[5]: core/contacts/4.creating_a_group -[6]: core/contacts/5.creating_a_smart_group -[7]: core/messages/1.getting_around_the_messages_tab -[8]: ../images/help/poll1.png +[1]: ../activities/3.creating_a_poll +[3]: ../activities/5.creating_an_auto-reply +[4]: ../activities/4.creating_an_announcement +[5]: ../contacts/4.creating_a_group +[6]: ../contacts/5.creating_a_smart_group +[7]: ../messages/1.getting_around_the_messages_tab +[8]: ../images/help/autoforward1.png [9]: ../images/help/poll2.png [10]: ../images/help/poll3.png [11]: ../images/help/poll4.png @@ -152,4 +128,4 @@ Your poll will be displayed under the "Activities" sub heading in the [messages [14]: ../images/help/poll7.png [15]: ../images/help/poll8.png [16]: ../images/help/poll9.png -[17]: ../images/help/aliases.png +[17]: ../images/help/pollkeywords.png diff --git a/plugins/frontlinesms-core/grails-app/conf/help/activities/3a.manually_categorising.txt b/plugins/frontlinesms-core/grails-app/conf/help/activities/3a.manually_categorising.txt new file mode 100644 index 000000000..0da3d89cb --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/conf/help/activities/3a.manually_categorising.txt @@ -0,0 +1,29 @@ +# Manually Categorizing a Message + +A Poll will attempt to automatically categorize incoming responses according to the message content. This allows FrontlineSMS to show you the results of polls in easy-to-read tables and visually represented as charts. If the keyword is not present or incorrectly spelled, the system will not recognize responses and may not be able to automatically categorize them. In these cases, it is possible to manually categorize a message, or multiple messages under the correct response. + +![Manually Categorize a Response][6] + +### Manually Categorizing a Message/Messages + +1. Select the Activity you want to work with. +2. From the message list, select the message or messages you wish to categorize using the checkboxes. +3. From the message details, open the drop down box called "Categorize Response". +4. From this list choose the appropriate response for the message or messages. + +_**Note:** For [Yes/No Polls][2], the only categories available will be Yes or No. For [Multiple Choice Polls][2] the available categories will be the same in number and name as the multiple choice options you entered when creating the Poll._ + +Your message(s) will be updated with new categories and the poll statistics will be updated accordingly. + +### Related Actions +[Renaming an Activity][3] +[Creating a Poll][2] +[Creating an Announcement][4] +[Exporting an Activity][5] + +[1]: ../activities/3a.manually_categorising +[2]: ../activities/3.creating_a_poll +[3]: ../activities/6.renaming_an_activity +[4]: ../activities/4.creating_an_announcement +[5]: ../activities/7.exporting_an_activity +[6]: ../images/help/manual_response.png diff --git a/plugins/frontlinesms-core/web-app/help/core/activities/4.creating_an_announcement.txt b/plugins/frontlinesms-core/grails-app/conf/help/activities/4.creating_an_announcement.txt similarity index 88% rename from plugins/frontlinesms-core/web-app/help/core/activities/4.creating_an_announcement.txt rename to plugins/frontlinesms-core/grails-app/conf/help/activities/4.creating_an_announcement.txt index 340b97367..6f1e4fc17 100644 --- a/plugins/frontlinesms-core/web-app/help/core/activities/4.creating_an_announcement.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/activities/4.creating_an_announcement.txt @@ -62,14 +62,14 @@ Your Announcement will appear under the activities sub-heading. [Creating a Group][8] [Creating a Smart Group][5] -[1]: core/activities/4.creating_an_announcement -[2]: core/activities/4.creating_an_announcement -[3]: core/messages/1.getting_around_the_messages_tab -[4]: core/contacts/1.getting_around_the_contacts_tab -[5]: core/contacts/5.creating_a_smart_group -[6]: core/activities/1.getting_around_activities -[7]: core/activities/3.creating_a_poll -[8]: core/contacts/4.creating_a_group +[1]: ../activities/4.creating_an_announcement +[2]: ../activities/4.creating_an_announcement +[3]: ../messages/1.getting_around_the_messages_tab +[4]: ../contacts/1.getting_around_the_contacts_tab +[5]: ../contacts/5.creating_a_smart_group +[6]: ../activities/1.getting_around_activities +[7]: ../activities/3.creating_a_poll +[8]: ../contacts/4.creating_a_group [9]: ../images/help/poll1.png [10]: ../images/help/announcement1.png [11]: ../images/help/announcement2.png diff --git a/plugins/frontlinesms-core/grails-app/conf/help/activities/5.creating_an_auto-reply.txt b/plugins/frontlinesms-core/grails-app/conf/help/activities/5.creating_an_auto-reply.txt new file mode 100644 index 000000000..f156c0869 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/conf/help/activities/5.creating_an_auto-reply.txt @@ -0,0 +1,73 @@ +# Creating an AutoReply # + +An autoreply is a type of activity which allows you to set up FrontlineSMS to automatically send replies to messages as they are received. Autoreplies can be set up to work globally, on all messages that are received, or they can be set up to be triggered by messages containing [keywords]. + +Received messages that trigger an autoreply based on a keyword will be moved into the autoreply activity along with a copy of the autoreply that was sent. + +A global autoreply will copy incoming messages from the [inbox][2] to the autoreply activity, ensuring that the original message isn't lost. + +Auto-replies would be useful as an Out of Office message, to provide an acknowledgement of receipt, or to disseminate information on demand. For example, they could be set up to provide medical information for illnesses or symptoms set up as keywords in the system. + +### [How to Create an AutoReply][3] + +1. From the [messages tab][2] click on 'Create New Activity'. +2. A new window will appear and you will be presented with a number of options. Choose "AutoReply" and click next. +![Select Activity][9] + +## Automatic Sorting +![Auto sorting][10] + +On the next tab, you can configure FrontlineSMS to automatically process an autoreply message when a certain keyword is received; not to use a keyword, in which case the autoreply is only triggered when messages are manually moved into your Autoreply; or to respond to all incoming messages with an autoreply. Only one activity at a time can have this setting. + +Select your desired option. In the lower half of this dialogue, you can set the keyword, if applicable. You can enter any number of keywords separated by commas, and any incoming message that starts with one of those keywords will trigger the autoreply. This means that you can cater for spelling errors, different languages, and other reasons for differing keywords. + +_**Note:** Top Level Keywords are **unique.** Once a top level keyword has been used in an activity it cannot be used in another activity, unless that activity is archived or deleted._ + +When you have finished setting up your automatic processing options, click next. + +**_Enter Message_** + +![Enter Message][11] + +On this screen you will be able to enter the message you would like to use as the autoreply. + +You can also add expressions to the message to add a more personal touch. + +**_Summary Screen_** + +On this screen you will find a summary of all the options you have selected, so you can confirm that what you have created is what you would like to send. + +![Summary][12] + +You will also be able to name your autoreply so that it is easily recognizable. + +_**Note:** Activity names must be unique. You cannot have two activities with the same name. When an activity is deleted **and** removed from the trash, that name can then be used in any new activities._ + +Clicking next will create the [activity][5]. + +**_Confirmation_** + +Once successfully created, you will see a confirmation screen like this one: + +![Confirmation][13] + +Clicking OK will take you back to the [Messages Tab][2] + +Your Auto-reply will appear under the Activities sub-heading. + +### Related Actions +[Creating a Poll][6] +[Creating a Group][7] +[Creating a Smart Group][8] + +[2]: ../messages/1.getting_around_the_messages_tab +[3]: ../activities/5.creating_an_auto-reply +[5]: ../activities/1.getting_around_activities +[6]: ../activities/3.creating_a_poll +[7]: ../contacts/4.creating_a_group +[8]: ../contacts/5.creating_a_smart_group +[9]: ../images/help/poll1.png +[10]: ../images/help/autoreply1.png +[11]: ../images/help/autoreply2.png +[12]: ../images/help/autoreply3.png +[13]: ../images/help/autoreply4.png diff --git a/plugins/frontlinesms-core/web-app/help/core/activities/6.renaming_an_activity.txt b/plugins/frontlinesms-core/grails-app/conf/help/activities/6.renaming_an_activity.txt similarity index 78% rename from plugins/frontlinesms-core/web-app/help/core/activities/6.renaming_an_activity.txt rename to plugins/frontlinesms-core/grails-app/conf/help/activities/6.renaming_an_activity.txt index f45c9bf2e..53b8cf38d 100644 --- a/plugins/frontlinesms-core/web-app/help/core/activities/6.renaming_an_activity.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/activities/6.renaming_an_activity.txt @@ -22,10 +22,10 @@ _**Note:** Activity names must be unique. You cannot have two activities with th [Creating an Announcement][4] [Manually Categorising a Response][5] -[1]: core/activities/6.renaming_an_activity -[2]: core/activities/7.exporting_an_activity -[3]: core/activities/3.creating_a_poll -[4]: core/activities/4.creating_an_announcement -[5]: core/activities/3a.manually_categorising +[1]: ../activities/6.renaming_an_activity +[2]: ../activities/7.exporting_an_activity +[3]: ../activities/3.creating_a_poll +[4]: ../activities/4.creating_an_announcement +[5]: ../activities/3a.manually_categorising [6]: ../images/help/rename_poll.png [7]: ../images/help/rename_poll_dialog.png diff --git a/plugins/frontlinesms-core/web-app/help/core/activities/7.exporting_an_activity.txt b/plugins/frontlinesms-core/grails-app/conf/help/activities/7.exporting_an_activity.txt similarity index 78% rename from plugins/frontlinesms-core/web-app/help/core/activities/7.exporting_an_activity.txt rename to plugins/frontlinesms-core/grails-app/conf/help/activities/7.exporting_an_activity.txt index fdce3d2dd..95312631b 100644 --- a/plugins/frontlinesms-core/web-app/help/core/activities/7.exporting_an_activity.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/activities/7.exporting_an_activity.txt @@ -21,10 +21,10 @@ Your messages will be exported to the desired format. [Creating an Announcement][4] [Manually Categorising a Response][5] -[1]: core/activities/7.exporting_an_activity -[2]: core/activities/7.exporting_an_activity -[3]: core/activities/3.creating_a_poll -[4]: core/activities/4.creating_an_announcement -[5]: core/activities/3a.manually_categorising +[1]: ../activities/7.exporting_an_activity +[2]: ../activities/7.exporting_an_activity +[3]: ../activities/3.creating_a_poll +[4]: ../activities/4.creating_an_announcement +[5]: ../activities/3a.manually_categorising [6]: ../images/help/export_activity.png [7]: ../images/help/export_dialog.png diff --git a/plugins/frontlinesms-core/grails-app/conf/help/activities/8.creating_an_autoforward.txt b/plugins/frontlinesms-core/grails-app/conf/help/activities/8.creating_an_autoforward.txt new file mode 100644 index 000000000..0b94498bf --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/conf/help/activities/8.creating_an_autoforward.txt @@ -0,0 +1,72 @@ +# Creating an Autoforward + +An autoforward activity allows you to automatically forward incoming SMS to selected contacts via SMS. Autoforwards can set up to be global, thereby acting on all incoming messages, or they can be set up so that only messages matching one of the configured keywords are forwarded. The message sent to the selected recipients can be configured to contain more information than just the original message content. Both incoming messages that triggered the Autoforward and the resultant outgoing messages are stored under the Autoforward activity for ease of access. When setting up an autoforward, you can select any number of individual contacts, groups or smart groups to forward to. Autoforwards can be used to enable communication between members of a group, and can form part of a opt-in information service when used together with [subscriptions][1] + +### How to Create an AutoForward + +From the [messages tab][2] click on 'Create New Activity'. + +![Select Activity][7] + +A new window will appear and you will be presented with a number of options. Choose "Autoforward" and click next. + +![Enter Message][8] + +In the first step you can configure the message that will be sent when an incoming message triggers an Autoforward. There are various substitution expressions, available through the 'magic wand' button, which you can use to populate the message content. The default value, '${message_text}', will forward incoming messages without any changes. + +**Automatic Processing** +![Auto sorting][9] + +On the next tab, you can configure FrontlineSMS to automatically trigger your Autoforward on incoming messages. With the 'Process responses containing a keyword automatically' option, you can enter any number of keywords separated by commas. With this setting, any incoming message that starts with one of those keywords will trigger the Autoforward. + +Alternatively, you can select the 'Do not use a keyword' setting, which will result in all incoming messages triggering the Autoforward, provided they do not match any other activities' keywords. Only one activity at a time can have this setting. + +If you choose to, you can also disable automatic sorting altogether. With this setting, you can still use your Autoforward's functionality by manually moving messages into your new activity from the inbox or any other activity or folder. + +_**Note:** Top Level Keywords are **unique.** Once a top level keyword has been used in an activity it cannot be used in another activity, unless that activity is archived or deleted._ + +When you have finished setting up your automatic sorting options, click next. + +**Select Recipients** + +![Select Recipients][10] + +In this step, you can select the recipients you want your Autoforward to send messages to. A list of all your saved contacts, groups and smart groups is available to choose from, and you can search the list using the search bar at the bottom. Using the text field above the list, you can also manually enter any number of phone numbers to forward messages to. When using groups or smart groups in an Autoforward, FrontlineSMS will forward the message to the current members of the group at the time when the incoming message arrives. This means that after setting up your Autoforward you can add and remove members from your groups, possibly using a [Subscription][1], and all the latest members will receive messages from the Autoforward. + +Once you have populated your recipient list, click next. + +**Confirm Screen** + +On this screen you will find a summary of all the options you have selected, so you can confirm that you entered the correct settings before creating the activity. You will also be prompted to give a name to your Autoforward to make it easily recognisable from the list of activities on the messages tab. + +![Confirm][11] + +Clicking Create will save the [activity][3]. + +**_Confirmation_** + +Once successfully created, you will see a confirmation screen. This means that your Autoforward has successfully been saved and will now actively forward incoming messages according to your automatic sorting rules. If there were any errors when saving the activity, a yellow error message will prompt you about the changes required. This will usually be because you have reused a keyword that another activity is using, or given the Autoforward the same name as an existing activity. Once you correct any of these issues, click 'Create' again to save the Autoforward. + +![Summary][12] + +Clicking OK will take you back to the [Messages Tab][2] + +Your Autoforward will appear under the Activities sub-heading. + +### Related Actions +[Creating a Poll][4] +[Creating a Group][5] +[Creating a Smart Group][6] + +[1]: ../activities/9.creating_a_subscription +[2]: ../messages/1.getting_around_the_messages_tab +[3]: ../activities/1.getting_around_activities +[4]: ../activities/3.creating_a_poll +[5]: ../contacts/4.creating_a_group +[6]: ../contacts/5.creating_a_smart_group +[7]: ../images/help/autoforward1.png +[8]: ../images/help/autoforward2.png +[9]: ../images/help/autoforward3.png +[10]: ../images/help/autoforward4.png +[11]: ../images/help/autoforward5.png +[12]: ../images/help/autoforward6.png diff --git a/plugins/frontlinesms-core/grails-app/conf/help/activities/9.creating_a_subscription.txt b/plugins/frontlinesms-core/grails-app/conf/help/activities/9.creating_a_subscription.txt new file mode 100644 index 000000000..35de6da23 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/conf/help/activities/9.creating_a_subscription.txt @@ -0,0 +1,94 @@ +# Creating a Subscription # + +A subscription is a type of activity that allows users to join of leave a specified group based on the content of the message that they text in. Keywords can trigger either adding the contact to the group, removing the contact from the group or toggling the group membership of the contact. + +### How to Create a Subscription + +From the [messages tab][2] click on 'Create New Activity'. +A new window will appear and you will be presented with a number of options. Choose "Subscription" and click next + +![Select Activity][7] + +#### Select a Group +You can select the group that contacts will join or leave. + +![Select A Group][8] + +When you have selected the desired group click next. + +#### Automatic Processing +![Automatic Sorting][9] + +On the next tab, you can configure FrontlineSMS to automatically trigger your Subscription activity for incoming messages. With the 'Sort responses automatically' option, you can enter keywords to trigger join, leave or toggle actions. + +Alternatively, you can select the 'Don't sort messages automatically' setting. If you choose this option, then only messages you manually move into this activity will be processed. To sort using a keyword, select 'sort responses automatically'. + +With automatic sorting enabled, you will be able to set keywords to trigger the activity. + +_**Note:** Top Level Keywords are **unique.** Once a top level keyword has been used in an activity it cannot be used in another activity, unless that activity is archived or deleted. In Subscriptions, top-level responses are optional. If you decide to allow respondents to text in only one word to join or leave a group, i.e. not to provide a top-level keyword, then the response keywords are treated as top-level and must be unique across all the activity top level keywords._ + +For any keyword, including each response, you can enter any number of keywords separated by commas. With this setting, any incoming message that starts with one of those keywords will trigger the activity. This means that you can cater for spelling errors, different languages, and other reasons for differing keywords. + +A subscription needs keywords that will match to the join action (Contact will join the group) and keywords that will match the leave action (Contact will leave the group). The keywords for subscription actions are not optional and need to filled in. The keywords in a subscription, inclucing top level and subscription action keywords, need to be unique within that subscription. + +Subscriptions allow a default action to be triggered if a message matched a top level keyword and did not match at least one subscription action keyword. + +![Select Default Action][10] + +Take for example a subscription where sorting is enabled and keywords look as follows. + + Top level keywords : 'team, tim, tom' + Join Keywords : 'join, in, inthere' + Leave Keywords : 'leave, out, outthere' + Default Action : Add Contact to the Group + +The incoming message will be mapped to actions as illustrated below + + + Incoming Message | Action Triggered + team join | Joined Group + team in | Joined Group + team inthere | Joined Group + tim join | Joined Group + tim in | Joined Group + tim inthere | Joined Group + team leave | Joined Group + team out | Joined Group + team outhere | Joined Group + team | Default Action + tim | Default Action + tom | Default Action + + +#### Send Autoreplies +![Autoreplies][11] + +On this tab you can setup the autoreplies that will be sent to the contact after they have joined or left a group. +You can set up seperate messages, one that is sent when a contact joins the group and one that is sent when a contact leaves the group. + +Setting up this messages is optional. + +#### Confirm Screen +![Confirm Screen][12] + +On this screen you will find a summary of all the options you have selected, so you can confirm that what you have created is what you would like to send. + +On this screen you can edit the name of the subscription. The name needs to be unique among other subscriptions. Two subscriptions cannot share the same name. + +### Related Actions +[Creating a Poll][4] +[Creating a Group][5] +[Creating a Smart Group][6] + +[1]: ../activities/9.creating_a_subscription +[2]: ../messages/1.getting_around_the_messages_tab +[3]: ../activities/1.getting_around_activities +[4]: ../activities/3.creating_a_poll +[5]: ../contacts/4.creating_a_group +[6]: ../contacts/5.creating_a_smart_group +[7]: ../images/help/autoforward1.png +[8]: ../images/help/subscription1.png +[9]: ../images/help/subscription2.png +[10]: ../images/help/subscription3.png +[11]: ../images/help/subscription4.png +[12]: ../images/help/subscription5.png diff --git a/plugins/frontlinesms-core/web-app/help/core/archive/1.getting_around_the_archive_tab.txt b/plugins/frontlinesms-core/grails-app/conf/help/archive/1.getting_around_the_archive_tab.txt similarity index 77% rename from plugins/frontlinesms-core/web-app/help/core/archive/1.getting_around_the_archive_tab.txt rename to plugins/frontlinesms-core/grails-app/conf/help/archive/1.getting_around_the_archive_tab.txt index c48bc5d94..9599ff9e2 100644 --- a/plugins/frontlinesms-core/web-app/help/core/archive/1.getting_around_the_archive_tab.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/archive/1.getting_around_the_archive_tab.txt @@ -29,14 +29,14 @@ You can [reply][9], [move][9], [forward][9], [delete][9] and [unarchive][12] mes [Archiving Messages][11] [Unarchiving Messages, Activities and Folders][12] -[1]: core/archive/1.getting_around_the_archive_tab -[2]: core/archive/1a.inbox_archive -[3]: core/archive/1b.sent_archive -[4]: core/archive/1c.activity_archive -[5]: core/archive/1d.folder_archive -[7]: core/messages/2.sss -[9]: core/messages/8.mrfd -[10]: core/archive/3.archiving_activities_folders -[11]: core/archive/2.archiving_messages -[12]: core/archive/4.unarchive +[1]: ../archive/1.getting_around_the_archive_tab +[2]: ../archive/1a.inbox_archive +[3]: ../archive/1b.sent_archive +[4]: ../archive/1c.activity_archive +[5]: ../archive/1d.folder_archive +[7]: ../messages/2.sss +[9]: ../messages/8.mrfd +[10]: ../archive/3.archiving_activities_folders +[11]: ../archive/2.archiving_messages +[12]: ../archive/4.unarchive [13]: ../images/help/archive_overview.png diff --git a/plugins/frontlinesms-core/web-app/help/core/archive/1a.inbox_archive.txt b/plugins/frontlinesms-core/grails-app/conf/help/archive/1a.inbox_archive.txt similarity index 67% rename from plugins/frontlinesms-core/web-app/help/core/archive/1a.inbox_archive.txt rename to plugins/frontlinesms-core/grails-app/conf/help/archive/1a.inbox_archive.txt index 8233c182e..f4f827802 100644 --- a/plugins/frontlinesms-core/web-app/help/core/archive/1a.inbox_archive.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/archive/1a.inbox_archive.txt @@ -21,16 +21,16 @@ When a message is selected (2), the full text will be displayed here along with [Archiving Messages][12] [Unarchiving Messages, Activities and Folders][10] -[1]: core/archive/1a.inbox_archive -[2]: core/archive/1.getting_around_the_archive_tab -[3]: core/archive/1a.inbox_archive -[4]: core/messages/3.quick_message -[5]: core/messages/9.exporting -[6]: core/archive/1a.inbox_archive -[7]: core/messages/2.sss -[8]: core/archive/1a.inbox_archive -[9]: core/messages/8.mrfd -[10]: core/archive/4.unarchive -[11]: core/archive/3.archiving_activities_folders -[12]: core/archive/2.archiving_messages +[1]: ../archive/1a.inbox_archive +[2]: ../archive/1.getting_around_the_archive_tab +[3]: ../archive/1a.inbox_archive +[4]: ../messages/3.quick_message +[5]: ../messages/9.exporting +[6]: ../archive/1a.inbox_archive +[7]: ../messages/2.sss +[8]: ../archive/1a.inbox_archive +[9]: ../messages/8.mrfd +[10]: ../archive/4.unarchive +[11]: ../archive/3.archiving_activities_folders +[12]: ../archive/2.archiving_messages [13]: ../images/help/inbox_archive.png diff --git a/plugins/frontlinesms-core/web-app/help/core/archive/1b.sent_archive.txt b/plugins/frontlinesms-core/grails-app/conf/help/archive/1b.sent_archive.txt similarity index 67% rename from plugins/frontlinesms-core/web-app/help/core/archive/1b.sent_archive.txt rename to plugins/frontlinesms-core/grails-app/conf/help/archive/1b.sent_archive.txt index 69f33ba3a..c4e7aab3b 100644 --- a/plugins/frontlinesms-core/web-app/help/core/archive/1b.sent_archive.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/archive/1b.sent_archive.txt @@ -21,17 +21,17 @@ When a message is selected, the full text will be displayed here (2) along with [Archiving Messages][12] [Unarchiving Messages, Activities and Folders][10] -[1]: core/archive/1b.sent_archive -[2]: core/messages/5.sent -[3]: core/archive/1b.sent_archive -[4]: core/messages/3.quick_message -[5]: core/messages/9.exporting -[6]: core/archive/1b.sent_archive -[7]: core/messages/2.sss -[8]: core/archive/1b.sent_archive -[9]: core/messages/8.mrfd -[10]: core/archive/4.unarchive -[11]: core/archive/3.archiving_activities_folders -[12]: core/archive/2.archiving_messages +[1]: ../archive/1b.sent_archive +[2]: ../messages/5.sent +[3]: ../archive/1b.sent_archive +[4]: ../messages/3.quick_message +[5]: ../messages/9.exporting +[6]: ../archive/1b.sent_archive +[7]: ../messages/2.sss +[8]: ../archive/1b.sent_archive +[9]: ../messages/8.mrfd +[10]: ../archive/4.unarchive +[11]: ../archive/3.archiving_activities_folders +[12]: ../archive/2.archiving_messages [13]: ../images/help/sent_archive.png diff --git a/plugins/frontlinesms-core/web-app/help/core/archive/1c.activity_archive.txt b/plugins/frontlinesms-core/grails-app/conf/help/archive/1c.activity_archive.txt similarity index 80% rename from plugins/frontlinesms-core/web-app/help/core/archive/1c.activity_archive.txt rename to plugins/frontlinesms-core/grails-app/conf/help/archive/1c.activity_archive.txt index cfd718445..bfdb8559e 100644 --- a/plugins/frontlinesms-core/web-app/help/core/archive/1c.activity_archive.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/archive/1c.activity_archive.txt @@ -23,14 +23,14 @@ Clicking on a single activity will take you to that activities detail view which [Archiving Messages][9] [Unarchiving Messages, Activities and Folders][3] -[1]: core/archive/1.getting_around_the_archive_tab -[2]: core/archive/1c.activity_archive -[3]: core/archive/4.unarchive -[4]: core/messages/8.mrfd -[5]: core/messages/9.exporting -[6]: core/archive/1c.activity_archive -[7]: core/archive/1.getting_around_the_archive_tab -[8]: core/archive/3.archiving_activities_folders -[9]: core/archive/2.archiving_messages +[1]: ../archive/1.getting_around_the_archive_tab +[2]: ../archive/1c.activity_archive +[3]: ../archive/4.unarchive +[4]: ../messages/8.mrfd +[5]: ../messages/9.exporting +[6]: ../archive/1c.activity_archive +[7]: ../archive/1.getting_around_the_archive_tab +[8]: ../archive/3.archiving_activities_folders +[9]: ../archive/2.archiving_messages [10]: ../images/help/activity_archive.png [11]: ../images/help/activity_archive_detail.png diff --git a/plugins/frontlinesms-core/web-app/help/core/archive/1d.folder_archive.txt b/plugins/frontlinesms-core/grails-app/conf/help/archive/1d.folder_archive.txt similarity index 83% rename from plugins/frontlinesms-core/web-app/help/core/archive/1d.folder_archive.txt rename to plugins/frontlinesms-core/grails-app/conf/help/archive/1d.folder_archive.txt index 888d3ae1c..f00e1743a 100644 --- a/plugins/frontlinesms-core/web-app/help/core/archive/1d.folder_archive.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/archive/1d.folder_archive.txt @@ -24,12 +24,12 @@ Clicking on a single folder will take you to that folders detail view which rese [Unarchiving Messages, Activities and Folders][2] [Moving Messages into a Folder][3] -[2]: core/archive/4.unarchive -[3]: core/messages/8.mrfd -[4]: core/messages/9.exporting -[5]: core/folders/2.creating_a_folder -[6]: core/folders/1.getting_around_folders -[7]: core/archive/3.archiving_activities_folders -[8]: core/archive/2.archiving_messages +[2]: ../archive/4.unarchive +[3]: ../messages/8.mrfd +[4]: ../messages/9.exporting +[5]: ../folders/2.creating_a_folder +[6]: ../folders/1.getting_around_folders +[7]: ../archive/3.archiving_activities_folders +[8]: ../archive/2.archiving_messages [9]: ../images/help/folder_archive.png [10]: ../images/help/folder_archive_detail.png diff --git a/plugins/frontlinesms-core/web-app/help/core/archive/2.archiving_messages.txt b/plugins/frontlinesms-core/grails-app/conf/help/archive/2.archiving_messages.txt similarity index 77% rename from plugins/frontlinesms-core/web-app/help/core/archive/2.archiving_messages.txt rename to plugins/frontlinesms-core/grails-app/conf/help/archive/2.archiving_messages.txt index ec83271e3..207c02e3e 100644 --- a/plugins/frontlinesms-core/web-app/help/core/archive/2.archiving_messages.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/archive/2.archiving_messages.txt @@ -21,16 +21,16 @@ Messages archived from [sent messages][10] will be found in the [sent archive][1 [Archiving Activities and Folders][8] [Unarchiving Messages, Activities and Folders][12] -[1]: core/archive/1.getting_around_the_archive_tab -[2]: core/archive/1a.inbox_archive -[3]: core/messages/8.mrfd -[4]: core/archive/2.archiving_messages -[5]: core/messages/2.sss -[6]: core/activities/1.getting_around_activities -[7]: core/folders/1.getting_around_folders -[8]: core/archive/3.archiving_activities_folders -[9]: core/archive/1a.inbox_archive -[10]: core/messages/5.sent -[11]: core/archive/1b.sent_archive -[12]: core/archive/4.unarchive +[1]: ../archive/1.getting_around_the_archive_tab +[2]: ../archive/1a.inbox_archive +[3]: ../messages/8.mrfd +[4]: ../archive/2.archiving_messages +[5]: ../messages/2.sss +[6]: ../activities/1.getting_around_activities +[7]: ../folders/1.getting_around_folders +[8]: ../archive/3.archiving_activities_folders +[9]: ../archive/1a.inbox_archive +[10]: ../messages/5.sent +[11]: ../archive/1b.sent_archive +[12]: ../archive/4.unarchive [13]: ../images/help/mrfd_reply.png diff --git a/plugins/frontlinesms-core/web-app/help/core/archive/3.archiving_activities_folders.txt b/plugins/frontlinesms-core/grails-app/conf/help/archive/3.archiving_activities_folders.txt similarity index 78% rename from plugins/frontlinesms-core/web-app/help/core/archive/3.archiving_activities_folders.txt rename to plugins/frontlinesms-core/grails-app/conf/help/archive/3.archiving_activities_folders.txt index 4e0c081c2..5c095ec91 100644 --- a/plugins/frontlinesms-core/web-app/help/core/archive/3.archiving_activities_folders.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/archive/3.archiving_activities_folders.txt @@ -31,17 +31,17 @@ To view your archived folder, navigate to the [archive tab][4] and then click on [Creating a Poll][10] [Creating an Announcement][14] -[1]: core/activities/1.getting_around_activities -[2]: core/messages/1.getting_around_the_messages_tab -[3]: core/archive/1d.folder_archive -[4]: core/archive/1.getting_around_the_archive_tab -[5]: core/messages/9.exporting -[6]: core/activities/6.renaming_an_activity -[7]: core/messages/8.mrfd -[8]: core/archive/1c.activity_archive -[10]: core/activities/3.creating_a_poll -[11]: core/archive/3.archiving_activities_folders -[12]: core/folders/1.getting_around_folders -[13]: core/archive/4.unarchive -[14]: core/activities/4.creating_an_announcement +[1]: ../activities/1.getting_around_activities +[2]: ../messages/1.getting_around_the_messages_tab +[3]: ../archive/1d.folder_archive +[4]: ../archive/1.getting_around_the_archive_tab +[5]: ../messages/9.exporting +[6]: ../activities/6.renaming_an_activity +[7]: ../messages/8.mrfd +[8]: ../archive/1c.activity_archive +[10]: ../activities/3.creating_a_poll +[11]: ../archive/3.archiving_activities_folders +[12]: ../folders/1.getting_around_folders +[13]: ../archive/4.unarchive +[14]: ../activities/4.creating_an_announcement [15]: ../images/help/archive_activity.png diff --git a/plugins/frontlinesms-core/web-app/help/core/archive/4.unarchive.txt b/plugins/frontlinesms-core/grails-app/conf/help/archive/4.unarchive.txt similarity index 77% rename from plugins/frontlinesms-core/web-app/help/core/archive/4.unarchive.txt rename to plugins/frontlinesms-core/grails-app/conf/help/archive/4.unarchive.txt index 1a4959965..bd0e5df99 100644 --- a/plugins/frontlinesms-core/web-app/help/core/archive/4.unarchive.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/archive/4.unarchive.txt @@ -47,20 +47,20 @@ Your folder will now appear in the [messages][1] tab. [Sending a Message][14] [Forwarding messages][15] -[1]: core/messages/1.getting_around_the_messages_tab -[2]: core/archive/2.archiving_messages -[3]: core/archive/1a.inbox_archive -[4]: core/archive/1.getting_around_the_archive_tab -[5]: core/archive/1b.sent_archive -[6]: core/archive/1b.sent_archive -[7]: core/messages/5.sent -[8]: core/archive/4.unarchive -[9]: core/archive/1c.activity_archive -[10]: core/archive/4.unarchive -[11]: core/archive/1d.folder_archive -[12]: core/folders/2.creating_a_folder -[13]: core/activities/2.creating_an_activity -[14]: core/messages/3.quick_message -[15]: core/messages/8.mrfd +[1]: ../messages/1.getting_around_the_messages_tab +[2]: ../archive/2.archiving_messages +[3]: ../archive/1a.inbox_archive +[4]: ../archive/1.getting_around_the_archive_tab +[5]: ../archive/1b.sent_archive +[6]: ../archive/1b.sent_archive +[7]: ../messages/5.sent +[8]: ../archive/4.unarchive +[9]: ../archive/1c.activity_archive +[10]: ../archive/4.unarchive +[11]: ../archive/1d.folder_archive +[12]: ../folders/2.creating_a_folder +[13]: ../activities/2.creating_an_activity +[14]: ../messages/3.quick_message +[15]: ../messages/8.mrfd [16]: ../images/help/unarchive_message.png [17]: ../images/help/activityunarchive.png diff --git a/plugins/frontlinesms-core/web-app/help/core/contacts/1.getting_around_the_contacts_tab.txt b/plugins/frontlinesms-core/grails-app/conf/help/contacts/1.getting_around_the_contacts_tab.txt similarity index 79% rename from plugins/frontlinesms-core/web-app/help/core/contacts/1.getting_around_the_contacts_tab.txt rename to plugins/frontlinesms-core/grails-app/conf/help/contacts/1.getting_around_the_contacts_tab.txt index f8e76514c..2c2a40fcf 100644 --- a/plugins/frontlinesms-core/web-app/help/core/contacts/1.getting_around_the_contacts_tab.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/contacts/1.getting_around_the_contacts_tab.txt @@ -36,14 +36,14 @@ The [search filter][10] allows you to quickly search through the displayed conta [Creating a Smart Group][2] [Editing a contact][7] -[1]: core/contacts/4.creating_a_group -[2]: core/contacts/5.creating_a_smart_group -[3]: core/contacts/1.getting_around_the_contacts_tab -[4]: core/contacts/2.add_contact -[5]: core/contacts/1.getting_around_the_contacts_tab -[6]: core/contacts/1.getting_around_the_contacts_tab -[7]: core/contacts/2a.editing_a_contact -[8]: core/contacts/6.add_remove_contact_to_from_a_group -[9]: core/contacts/1.getting_around_the_contacts_tab -[10]: core/contacts/8.searching_through_contacts +[1]: ../contacts/4.creating_a_group +[2]: ../contacts/5.creating_a_smart_group +[3]: ../contacts/1.getting_around_the_contacts_tab +[4]: ../contacts/2.add_contact +[5]: ../contacts/1.getting_around_the_contacts_tab +[6]: ../contacts/1.getting_around_the_contacts_tab +[7]: ../contacts/2a.editing_a_contact +[8]: ../contacts/6.add_remove_contact_to_from_a_group +[9]: ../contacts/1.getting_around_the_contacts_tab +[10]: ../contacts/8.searching_through_contacts [11]: ../images/help/contacts_overview.png diff --git a/plugins/frontlinesms-core/grails-app/conf/help/contacts/2.add_contact.txt b/plugins/frontlinesms-core/grails-app/conf/help/contacts/2.add_contact.txt new file mode 100644 index 000000000..2779a6845 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/conf/help/contacts/2.add_contact.txt @@ -0,0 +1,119 @@ +# Adding and Importing Contacts + +## Adding a Single Contact + +Adding a contact allows you to create contacts one at a time in FrontlineSMS. Instructions for importing multiple contacts can be found below. + +Once your contacts are in the system, you can use these details to easily identify those you are interacting with, [send messages][1], add recipients to [activities][2] and create [smart groups][3]. + +There are two ways to add a single contact to the FrontlineSMS system: the first is to add a contact in the [Contacts][4] tab which you can access from the top header. The second way is to click on the (plus icon here) icon that you will find next to numbers that are not currently in the FrontlineSMS system. Clicking on the (plus icon) icon will lead you to the [Contact][4] tab, and the process to add a contact is the same as described here. By using the second method, the telephone number will be already filled in for you. + +### 1. Setting up a New Contact +Navigate to the [Contacts][4] tab, which you can find in the [navigation tab][5] at the top of your screen. +There is a button which says "Create New Contact", which you can see in the screenshot below. +Clicking this will present you with an empty form that you will use to enter the details of your new contact. **Remember to click 'save'** + +![Adding a New Contact][11] + +__OR__ + +If you have clicked on a (plus icon) icon, you will automatically be brought to the contacts tab, with a new contact form set up for you, with the number already entered. + +### 2. Filling in the details + +__Custom Fields__ +The default information boxes that are displayed in each contact record allow you to input basic details including a contact's name, phone number and email address. The primary number is the default number which any messages will be sent to. + +>*__Technical Tips!__ +>The 'Name' field has a 255 character limit. +>The 'Notes' field has a 1024 character limit. + +You can add custom fields that allow you to specify more information that you need. For example, you may want to add a postal address, or information about which region the contact is from, or you may even want to add what the contact's favorite color is. + +>*__Technical Tips!__ +>The name of a custom field can only be 255 characters long. Similarly, once created, custom fields have a 255 character limit. + +To create a custom field: + +1. There is a drop down box which says 'Add more information' (2). This will add the selected field to each contact in your system offering you space to add additional information. +2. If none of these options are what you are looking for, then choose the "Create New" option in the drop down box. +3. A window will then appear, allowing you to enter a name for the new field. +4. If you want this new field to appear by default in all new contacts created from this point on, then check the box next to where it says "Make a default field". +5. Once you have named the field, it will now appear in your contact's details (and all other contacts if you have made it default) for you to fill out. + +Once a field has been created it can be accessed easily again from the drop down menu from any contact. The field can also be removed from a particular contact by clicking the 'x' next to the field. + +__Assigning Groups__ +You can assign contacts to one or multiple groups so that you can perform tasks on large numbers of recipients at once. For example, you may have a group of contacts entitled 'friends' to whom you can send a [quick message][1], set up a [poll][7] or an [announcement][8] without having to add their number's individually. +To assign groups to contacts you first need to have a group to assign it to. +To learn how to create a group click [here][6]. You can manage your groups by adding or removing multiple contacts. +Please click [here][9] for more information on how to do that. + +To add contacts to groups you have created from their contact record, follow these instructions: + +1. Next to the heading 'Group' there is a dropdown box called 'Add to Group...' +2. Clicking on this will bring up a list of groups that currently exist in the system. +3. Simply choose the group you wish to add the contact to and the group name will appear next to the 'Group' heading. +4. To add this contact to another group repeat the above steps. You can add as many groups as you want, there is no limit. + +If you want to remove a contact from a group, simply click the 'x' icon that appears next to the name of the group in the contact record. + +### 3. Saving and Modifying a Contact's Details + +Now that you have added all the information you want to be associated with this contact, clicking on the 'Save' button will commit your changes to memory. +Don't worry if you accidentally saved the wrong information or need to modify the contact's details, because you can edit this at any time. +The contact's details will be displayed and you will be able to change/add information. Once you have finished editing, click 'Save' again to commit your changes to memory. +If you change your mind about altering a contact's details and you want to leave them as they were, then clicking 'Cancel' instead of 'Save' will remove any changes you have made and return the details as they were before you started to make any changes. + +## Importing Multiple Contacts + +If you have a large number of contacts to import, then you can use a different mechanism for importing them into FrontlineSMS by following the instructions below. The important steps are highlighted in the screenshot below. + +![Importing Multiple Contacts][12] + +1. Make sure your column headings are correct. + +The column headings should follow the example in the screenshot below. Note that "group" is optional; it will be interpreted by FrontlineSMS as a group tag for the contact. + +**Note: the column headings must be exact (e.g. 'E-mail Address', not 'email address').** + +![Importing Multiple Contacts Headings][13] + +**Note: If you are importing using vCards, you only need to ensure that your contact's mobile number is correctly listed as a mobile number; FrontlineSMS will ignore other numbers.** + +2. Click on 'Settings' in the upper right-hand corner of the window. Then click on 'Import and Export.' + +3. Select the type of file you wish to import. You can choose .csv - a common database format, or a vCard / .vcf format which is used in many contact management systems. + +4. Select the file from your computer. If it is a vCard it will import at this point. If it is a .csv file, you will now have the chance to review your file, as per the screenshot below. Ensure that your column headings are accurate; they will appear as green if they are correct, or yellow if they are incorrect. Once you're happy with the column headings, click "Import all contacts." FrontlineSMS will tell you if your contacts imported successfully or now. + +![Review Contact Import][14] + +If your contact did not import successfully, you will receive a fail notice as pictured below. FrontlineSMS will ask if you want to download a spreadsheet of the failed contact entries so that you can correct the issues and attempt to import them again into FrontlineSMS. + +![Import Fail Notice][15] + + + +### Related Actions + +[Managing a group][9] +[Making a Smart Group][3] +[Viewing All Messages Sent/Received by Contact][10] + + +[1]: ../messages/3.quick_message +[2]: ../activities/1.getting_around_activities +[3]: ../contacts/5.creating_a_smart_group +[4]: ../contacts/1.getting_around_the_contacts_tab +[5]: ../messages/1.getting_around_the_messages_tab +[6]: ../contacts/4.creating_a_group +[7]: ../activities/3.creating_a_poll +[8]: ../activities/4.creating_an_announcement +[9]: ../contacts/6.add_remove_contact_to_from_a_group +[10]: ../contacts/7.messages_sent_or_received_by_contact +[11]: ../images/help/ContactNew.png +[12]: ../images/help/ContactImport.png +[13]: ../images/help/contact_import/Screenshot_5_20_13_4_24_PM.jpg +[14]: ../images/help/contact_import/ReviewContactImport.png +[15]: ../images/help/contact_import/ImportFailNotice.png diff --git a/plugins/frontlinesms-core/grails-app/conf/help/contacts/2a.editing_a_contact.txt b/plugins/frontlinesms-core/grails-app/conf/help/contacts/2a.editing_a_contact.txt new file mode 100644 index 000000000..ea33473e8 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/conf/help/contacts/2a.editing_a_contact.txt @@ -0,0 +1,39 @@ +# Editing a Contact + +On this page you will find out how to modify an existing contact. + +### Editing a contact + +To modify the information saved under a contact, you have to navigate to the selected contact in the [contacts tab][2] and make the changes. Your changes will then be saved automatically. + +1. Navigate to the [contacts tab][2]. +2. Select the contact you wish to edit by either finding it in the [contact list][3] OR using the [search][4] feature in the [contact footer][5]. +3. In the [contact details][6] area, make the necessary edits to the contact. +4. Once you have made the changes, the contact details will be saved automatically. You will also see the phone number auto-format and show the relevant country flag. If the country flag is incorrect, it's a good visual cue for you to check to make sure the phone number is correctly entered in an international format. **Your changes will be saved automatically.** + +![Edit Contacts Screen Shot](../images/help/ContactEdits.png) + +### Deleting a Contact + +To delete a contact navigate to the contact record and select "Delete this Contact". + +To delete multiple contacts, select the contacts you wish to delete and select "Delete All" as in the screenshot below. + +![Delete Multiple Contacts Screen Shot](../images/help/ContactMultipleDelete.png) + +### Related Actions +[Adding a New contact][7] +[Creating a Group][8] +[Creating a Smart Group][9] + +[2]: ../contacts/1.getting_around_the_contacts_tab +[3]: ../contacts/1.getting_around_the_contacts_tab +[4]: ../contacts/8.searching_through_contacts +[5]: ../contacts/1.getting_around_the_contacts_tab +[6]: ../contacts/1.getting_around_the_contacts_tab +[7]: ../contacts/2.add_contact +[8]: ../contacts/4.creating_a_group +[9]: ../contacts/5.creating_a_smart_group +[10]: ../contacts/2a.editing_a_contact +[11]: ../images/help/delete_contact.png +[12]: ../images/help/delete_multiple_contact.png diff --git a/plugins/frontlinesms-core/web-app/help/core/contacts/3.add_remove_a_custom_field.txt b/plugins/frontlinesms-core/grails-app/conf/help/contacts/3.add_remove_a_custom_field.txt similarity index 85% rename from plugins/frontlinesms-core/web-app/help/core/contacts/3.add_remove_a_custom_field.txt rename to plugins/frontlinesms-core/grails-app/conf/help/contacts/3.add_remove_a_custom_field.txt index 451f1044d..0a68805e3 100644 --- a/plugins/frontlinesms-core/web-app/help/core/contacts/3.add_remove_a_custom_field.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/contacts/3.add_remove_a_custom_field.txt @@ -37,13 +37,13 @@ _**Note:** This will **not** remove the custom field from the "Add more informat [Creating a Smart Group][9] [Creating a Group][10] -[1]: core/contacts/5.creating_a_smart_group -[2]: core/search/1.getting_around_the_search_tab -[3]: core/contacts/3.add_remove_a_custom_field -[4]: core/contacts/1.getting_around_the_contacts_tab -[6]: core/contacts/3.add_remove_a_custom_field -[7]: core/contacts/6.add_remove_contact_to_from_a_group -[9]: core/contacts/5.creating_a_smart_group -[10]: core/contacts/4.creating_a_group +[1]: ../contacts/5.creating_a_smart_group +[2]: ../search/1.getting_around_the_search_tab +[3]: ../contacts/3.add_remove_a_custom_field +[4]: ../contacts/1.getting_around_the_contacts_tab +[6]: ../contacts/3.add_remove_a_custom_field +[7]: ../contacts/6.add_remove_contact_to_from_a_group +[9]: ../contacts/5.creating_a_smart_group +[10]: ../contacts/4.creating_a_group [11]: ../images/help/add_more_info.png [12]: ../images/help/namecustomfield.png diff --git a/plugins/frontlinesms-core/web-app/help/core/contacts/4.creating_a_group.txt b/plugins/frontlinesms-core/grails-app/conf/help/contacts/4.creating_a_group.txt similarity index 77% rename from plugins/frontlinesms-core/web-app/help/core/contacts/4.creating_a_group.txt rename to plugins/frontlinesms-core/grails-app/conf/help/contacts/4.creating_a_group.txt index b36536ca1..bb9befcf3 100644 --- a/plugins/frontlinesms-core/web-app/help/core/contacts/4.creating_a_group.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/contacts/4.creating_a_group.txt @@ -20,12 +20,12 @@ Your new group should appear under 'Groups'. [Exporting Contacts][6] [Removing/Adding a Custom Field to a Contact][7] -[1]: core/contacts/6.add_remove_contact_to_from_a_group -[2]: core/messages/3.quick_message -[3]: core/contacts/4.creating_a_group -[4]: core/contacts/1.getting_around_the_contacts_tab -[5]: core/contacts/5.creating_a_smart_group -[6]: core/messages/9.exporting -[7]: core/contacts/3.add_remove_a_custom_field +[1]: ../contacts/6.add_remove_contact_to_from_a_group +[2]: ../messages/3.quick_message +[3]: ../contacts/4.creating_a_group +[4]: ../contacts/1.getting_around_the_contacts_tab +[5]: ../contacts/5.creating_a_smart_group +[6]: ../messages/9.exporting +[7]: ../contacts/3.add_remove_a_custom_field [8]: ../images/help/create_group.png [9]: ../images/help/create_group_dialog.png diff --git a/plugins/frontlinesms-core/web-app/help/core/contacts/5.creating_a_smart_group.txt b/plugins/frontlinesms-core/grails-app/conf/help/contacts/5.creating_a_smart_group.txt similarity index 88% rename from plugins/frontlinesms-core/web-app/help/core/contacts/5.creating_a_smart_group.txt rename to plugins/frontlinesms-core/grails-app/conf/help/contacts/5.creating_a_smart_group.txt index be303f496..4162cfe20 100644 --- a/plugins/frontlinesms-core/web-app/help/core/contacts/5.creating_a_smart_group.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/contacts/5.creating_a_smart_group.txt @@ -28,11 +28,11 @@ _**Note:** When using custom fields in smart group rules, contacts must also hav [Removing/Adding a Custom Field to a Contact][6] [Creating a Group][7] -[1]: core/contacts/5.creating_a_smart_group -[2]: core/contacts/1.getting_around_the_contacts_tab -[4]: core/contacts/6.add_remove_contact_to_from_a_group -[5]: core/messages/9.exporting -[6]: core/contacts/3.add_remove_a_custom_field -[7]: core/contacts/4.creating_a_group +[1]: ../contacts/5.creating_a_smart_group +[2]: ../contacts/1.getting_around_the_contacts_tab +[4]: ../contacts/6.add_remove_contact_to_from_a_group +[5]: ../messages/9.exporting +[6]: ../contacts/3.add_remove_a_custom_field +[7]: ../contacts/4.creating_a_group [8]: ../images/help/create_smartgroup.png [9]: ../images/help/smartgroup_dialog.png diff --git a/plugins/frontlinesms-core/web-app/help/core/contacts/6.add_remove_contact_to_from_a_group.txt b/plugins/frontlinesms-core/grails-app/conf/help/contacts/6.add_remove_contact_to_from_a_group.txt similarity index 89% rename from plugins/frontlinesms-core/web-app/help/core/contacts/6.add_remove_contact_to_from_a_group.txt rename to plugins/frontlinesms-core/grails-app/conf/help/contacts/6.add_remove_contact_to_from_a_group.txt index 3a39f6eb5..9f815146f 100644 --- a/plugins/frontlinesms-core/web-app/help/core/contacts/6.add_remove_contact_to_from_a_group.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/contacts/6.add_remove_contact_to_from_a_group.txt @@ -79,16 +79,16 @@ __Group List View__ [Removing/Adding a Custom Field to a Contact][9] [Creating a Group][10] -[1]: core/contacts/6.add_remove_contact_to_from_a_group -[2]: core/contacts/4.creating_a_group -[3]: core/contacts/6.add_remove_contact_to_from_a_group -[4]: core/contacts/1.getting_around_the_contacts_tab -[5]: core/contacts/6.add_remove_contact_to_from_a_group -[6]: core/contacts/6.add_remove_contact_to_from_a_group -[7]: core/contacts/5.creating_a_smart_group -[8]: core/messages/9.exporting -[9]: core/contacts/3.add_remove_a_custom_field -[10]: core/contacts/4.creating_a_group +[1]: ../contacts/6.add_remove_contact_to_from_a_group +[2]: ../contacts/4.creating_a_group +[3]: ../contacts/6.add_remove_contact_to_from_a_group +[4]: ../contacts/1.getting_around_the_contacts_tab +[5]: ../contacts/6.add_remove_contact_to_from_a_group +[6]: ../contacts/6.add_remove_contact_to_from_a_group +[7]: ../contacts/5.creating_a_smart_group +[8]: ../messages/9.exporting +[9]: ../contacts/3.add_remove_a_custom_field +[10]: ../contacts/4.creating_a_group [11]: ../images/help/add_to_group.png [12]: ../images/help/multiple_add_to_group.png [13]: ../images/help/remove_single_contact.png diff --git a/plugins/frontlinesms-core/web-app/help/core/contacts/7.messages_sent_or_received_by_contact.txt b/plugins/frontlinesms-core/grails-app/conf/help/contacts/7.messages_sent_or_received_by_contact.txt similarity index 77% rename from plugins/frontlinesms-core/web-app/help/core/contacts/7.messages_sent_or_received_by_contact.txt rename to plugins/frontlinesms-core/grails-app/conf/help/contacts/7.messages_sent_or_received_by_contact.txt index e7b19a4ed..2e6425b54 100644 --- a/plugins/frontlinesms-core/web-app/help/core/contacts/7.messages_sent_or_received_by_contact.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/contacts/7.messages_sent_or_received_by_contact.txt @@ -19,11 +19,11 @@ You can [export][5] these messages using the "Export" button. More details can b [Creating a Smart Group][8] [Creating a Group][9] -[2]: core/contacts/1.getting_around_the_contacts_tab -[3]: core/search/1.getting_around_the_search_tab -[5]: core/messages/9.exporting -[6]: core/contacts/6.add_remove_contact_to_from_a_group -[7]: core/messages/9.exporting -[8]: core/contacts/5.creating_a_smart_group -[9]: core/contacts/4.creating_a_group +[2]: ../contacts/1.getting_around_the_contacts_tab +[3]: ../search/1.getting_around_the_search_tab +[5]: ../messages/9.exporting +[6]: ../contacts/6.add_remove_contact_to_from_a_group +[7]: ../messages/9.exporting +[8]: ../contacts/5.creating_a_smart_group +[9]: ../contacts/4.creating_a_group [10]: ../images/help/view_sent_received_messages.png diff --git a/plugins/frontlinesms-core/web-app/help/core/contacts/8.searching_through_contacts.txt b/plugins/frontlinesms-core/grails-app/conf/help/contacts/8.searching_through_contacts.txt similarity index 77% rename from plugins/frontlinesms-core/web-app/help/core/contacts/8.searching_through_contacts.txt rename to plugins/frontlinesms-core/grails-app/conf/help/contacts/8.searching_through_contacts.txt index e0d9dd08a..b47026882 100644 --- a/plugins/frontlinesms-core/web-app/help/core/contacts/8.searching_through_contacts.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/contacts/8.searching_through_contacts.txt @@ -19,11 +19,11 @@ This search filter works by matching the contents of the 'Name' field of your co [Creating a Smart Group][6] [Creating a Group][7] -[1]: core/contacts/8.searching_through_contacts -[2]: core/contacts/1.getting_around_the_contacts_tab -[3]: core/contacts/8.searching_through_contacts -[4]: core/contacts/6.add_remove_contact_to_from_a_group -[5]: core/messages/9.exporting -[6]: core/contacts/5.creating_a_smart_group -[7]: core/contacts/4.creating_a_group +[1]: ../contacts/8.searching_through_contacts +[2]: ../contacts/1.getting_around_the_contacts_tab +[3]: ../contacts/8.searching_through_contacts +[4]: ../contacts/6.add_remove_contact_to_from_a_group +[5]: ../messages/9.exporting +[6]: ../contacts/5.creating_a_smart_group +[7]: ../contacts/4.creating_a_group [8]: ../images/help/contact_search.png diff --git a/plugins/frontlinesms-core/grails-app/conf/help/features/new.txt b/plugins/frontlinesms-core/grails-app/conf/help/features/new.txt new file mode 100644 index 000000000..1cae9a363 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/conf/help/features/new.txt @@ -0,0 +1,36 @@ +# New features in FrontlineSMS v 2.4 +## A new Android gateway! +### FrontlineSync +FrontlineSync is an Android app (running on v. 2.3 and up) which lets you easily send and receive messages to and from FrontlineSMS - even if your Android phone is in a different location from you. FrontlineSync also registers missed calls to the phone in your FrontlineSync or FrontlineCloud workspace. + +There are some basic requirements for this to work: your Android phone will need to be on, and have access both to a mobile signal and wifi or data signal, with an appropriate plan; also, you will need the FrontlineSync application to be installed and synced with FrontlineSMS. After that, any messages you send will be routed through your Android phone, and incoming messages will be routed back to your inbox. All with a local number! + +## New features +1. Now you can register Missed Calls in FrontlineSMS using FrontlineSync +2. Alongside SMSSync and USB modems, FrontlineSync provides another even more stable way of sending and receiving SMS +3. FrontlineSync has performed faster in our tests when sending multiple SMS +4. Import contacts using the vCard/VCF file format + +## General improvements and interface updates +1. The link to the Connections management screen was moved up into the top navigation to make it easier to access. The old 'Status' information is now positioned to in the Settings section under 'Usage Statistics' +2. There is now more comprehensive support (UCS2) of special characters such as Arabic when handling message sending, viewing and exporting +3. New Send Message window is now just 1 simple screen to send a Quick Message +4. Improvements to the contact import review screen allow faster and more detailed editing of contacts +5. Routing rules on the Connections tab are new and improved +6. We added a new icon set to associate objects in the app more clearly and consistently +7. Activities listed in the Messages view no longer have the type of Activity shown to save space +8. Added a short-cut in Activity builders to allow you to create new groups without having to navigate back to the Contacts section +9. Message count in top navigation is now grey when no new messages exist +10. You can now edit the name of a connection directly in the Connections list +11. While editing inline on the Contacts or Connections screen, you can now undo your edits with the ESC key + +## Important bug fixes + * Importing large numbers of contacts is now much more stable and works in the background. Meaning it returns the response once done and does not require you to wait. + * Some variable replacement was not working when using the 'magic wand' feature in Activities + * Notifications on new messages were sometimes counting from the wrong folders + * Various bug fixes in the contact and groups management interface + +## And stay tuned ... +We have improved our release process to ensure FrontlineSMS receives more bug fixes more quickly. + +[1]: http://support.frontlinesms.com diff --git a/plugins/frontlinesms-core/web-app/help/core/folders/1.getting_around_folders.txt b/plugins/frontlinesms-core/grails-app/conf/help/folders/1.getting_around_folders.txt similarity index 71% rename from plugins/frontlinesms-core/web-app/help/core/folders/1.getting_around_folders.txt rename to plugins/frontlinesms-core/grails-app/conf/help/folders/1.getting_around_folders.txt index 6605c104a..bdee972c5 100644 --- a/plugins/frontlinesms-core/web-app/help/core/folders/1.getting_around_folders.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/folders/1.getting_around_folders.txt @@ -24,14 +24,14 @@ You can [reply][9], [move][9], [forward][9] and [delete][9] messages from here. [Getting Around the Inbox][10] [Creating a Folder][11] -[1]: core/search/1.getting_around_the_search_tab -[2]: core/folders/1.getting_around_folders -[3]: core/messages/3.quick_message -[4]: core/archive/3.archiving_activities_folders -[6]: core/folders/1.getting_around_folders -[7]: core/messages/2.sss -[8]: core/messages/1.getting_around_the_messages_tab -[9]: core/messages/8.mrfd -[10]: core/messages/1.getting_around_the_messages_tab -[11]: core/folders/2.creating_a_folder +[1]: ../search/1.getting_around_the_search_tab +[2]: ../folders/1.getting_around_folders +[3]: ../messages/3.quick_message +[4]: ../archive/3.archiving_activities_folders +[6]: ../folders/1.getting_around_folders +[7]: ../messages/2.sss +[8]: ../messages/1.getting_around_the_messages_tab +[9]: ../messages/8.mrfd +[10]: ../messages/1.getting_around_the_messages_tab +[11]: ../folders/2.creating_a_folder [12]: ../images/help/folder_overview.png diff --git a/plugins/frontlinesms-core/web-app/help/core/folders/2.creating_a_folder.txt b/plugins/frontlinesms-core/grails-app/conf/help/folders/2.creating_a_folder.txt similarity index 78% rename from plugins/frontlinesms-core/web-app/help/core/folders/2.creating_a_folder.txt rename to plugins/frontlinesms-core/grails-app/conf/help/folders/2.creating_a_folder.txt index 619d69ba0..a65a26201 100644 --- a/plugins/frontlinesms-core/web-app/help/core/folders/2.creating_a_folder.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/folders/2.creating_a_folder.txt @@ -18,11 +18,11 @@ Your folder will be created and will be displayed underneath the "Folders" sub h [Creating an Activity][5] [Moving Messages into a Folder][6] -[1]: core/messages/1.getting_around_the_messages_tab -[2]: core/activities/1.getting_around_activities -[3]: core/folders/2.creating_a_folder -[4]: core/archive/3.archiving_activities_folders -[5]: core/activities/2.creating_an_activity -[6]: core/messages/8.mrfd +[1]: ../messages/1.getting_around_the_messages_tab +[2]: ../activities/1.getting_around_activities +[3]: ../folders/2.creating_a_folder +[4]: ../archive/3.archiving_activities_folders +[5]: ../activities/2.creating_an_activity +[6]: ../messages/8.mrfd [7]: ../images/help/creating_folder.png [8]: ../images/help/folder_dialog.png diff --git a/plugins/frontlinesms-core/web-app/help/core/messages/1.getting_around_the_messages_tab.txt b/plugins/frontlinesms-core/grails-app/conf/help/messages/1.getting_around_the_messages_tab.txt similarity index 77% rename from plugins/frontlinesms-core/web-app/help/core/messages/1.getting_around_the_messages_tab.txt rename to plugins/frontlinesms-core/grails-app/conf/help/messages/1.getting_around_the_messages_tab.txt index dc7d1f315..92f2c8b06 100644 --- a/plugins/frontlinesms-core/web-app/help/core/messages/1.getting_around_the_messages_tab.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/messages/1.getting_around_the_messages_tab.txt @@ -54,22 +54,22 @@ When multiple messages are selected, a summary of how many messages have been se [Getting around the Search Tab][11] [Getting around the Status Tab][10] -[1]: core/messages/5.sent -[2]: core/activities/1.getting_around_activities -[3]: core/folders/1.getting_around_folders -[4]: core/activities/2.creating_an_activity -[5]: core/folders/2.creating_a_folder -[6]: core/messages/8.mrfd -[7]: core/archive/1.getting_around_the_archive_tab -[9]: core/contacts/1.getting_around_the_contacts_tab -[10]: core/status/1.getting_around_the_status_tab -[11]: core/search/1.getting_around_the_search_tab -[12]: core/settings/1.getting_around_the_settings_menu -[13]: core/messages/1.getting_around_the_messages_tab -[14]: core/messages/1.getting_around_the_messages_tab -[15]: core/messages/7.pending -[16]: core/messages/6.trash -[17]: core/messages/1.getting_around_the_messages_tab -[18]: core/messages/2.sss -[19]: core/messages/1.getting_around_the_messages_tab +[1]: ../messages/5.sent +[2]: ../activities/1.getting_around_activities +[3]: ../folders/1.getting_around_folders +[4]: ../activities/2.creating_an_activity +[5]: ../folders/2.creating_a_folder +[6]: ../messages/8.mrfd +[7]: ../archive/1.getting_around_the_archive_tab +[9]: ../contacts/1.getting_around_the_contacts_tab +[10]: ../status/1.getting_around_the_status_tab +[11]: ../search/1.getting_around_the_search_tab +[12]: ../settings/1.getting_around_the_settings_menu +[13]: ../messages/1.getting_around_the_messages_tab +[14]: ../messages/1.getting_around_the_messages_tab +[15]: ../messages/7.pending +[16]: ../messages/6.trash +[17]: ../messages/1.getting_around_the_messages_tab +[18]: ../messages/2.sss +[19]: ../messages/1.getting_around_the_messages_tab [20]: ../images/help/message_tab_overview.png diff --git a/plugins/frontlinesms-core/web-app/help/core/messages/2.sss.txt b/plugins/frontlinesms-core/grails-app/conf/help/messages/2.sss.txt similarity index 92% rename from plugins/frontlinesms-core/web-app/help/core/messages/2.sss.txt rename to plugins/frontlinesms-core/grails-app/conf/help/messages/2.sss.txt index 1b74858f6..7d7e8aa43 100644 --- a/plugins/frontlinesms-core/web-app/help/core/messages/2.sss.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/messages/2.sss.txt @@ -56,14 +56,14 @@ How to Sort by Message Content [Sending a Quick Message][17] [Moving, Replying, Forwarding and Deleting][18] -[2]: core/messages/1.getting_around_the_messages_tab -[3]: core/messages/5.sent -[4]: core/messages/7.pending -[5]: core/messages/6.trash -[6]: core/activities/1.getting_around_activities -[7]: core/folders/1.getting_around_folders -[8]: core/archive/1.getting_around_the_archive_tab -[9]: core/search/1.getting_around_the_search_tab -[17]: core/messages/3.quick_message -[18]: core/messages/8.mrfd +[2]: ../messages/1.getting_around_the_messages_tab +[3]: ../messages/5.sent +[4]: ../messages/7.pending +[5]: ../messages/6.trash +[6]: ../activities/1.getting_around_activities +[7]: ../folders/1.getting_around_folders +[8]: ../archive/1.getting_around_the_archive_tab +[9]: ../search/1.getting_around_the_search_tab +[17]: ../messages/3.quick_message +[18]: ../messages/8.mrfd [19]: ../images/help/sss.png diff --git a/plugins/frontlinesms-core/web-app/help/core/messages/3.quick_message.txt b/plugins/frontlinesms-core/grails-app/conf/help/messages/3.quick_message.txt similarity index 88% rename from plugins/frontlinesms-core/web-app/help/core/messages/3.quick_message.txt rename to plugins/frontlinesms-core/grails-app/conf/help/messages/3.quick_message.txt index f54396da4..4a776a46c 100644 --- a/plugins/frontlinesms-core/web-app/help/core/messages/3.quick_message.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/messages/3.quick_message.txt @@ -59,15 +59,15 @@ Clicking 'send' will complete the action. [Viewing Pending Messages][10] [Creating an Announcement][11] -[1]: core/messages/1.getting_around_the_messages_tab -[2]: core/search/1.getting_around_the_search_tab -[3]: core/messages/3.quick_message -[5]: core/contacts/1.getting_around_the_contacts_tab -[6]: core/contacts/4.creating_a_group -[7]: core/contacts/5.creating_a_smart_group -[9]: core/activities/3.creating_a_poll -[10]: core/messages/7.pending -[11]: core/activities/4.creating_an_announcement +[1]: ../messages/1.getting_around_the_messages_tab +[2]: ../search/1.getting_around_the_search_tab +[3]: ../messages/3.quick_message +[5]: ../contacts/1.getting_around_the_contacts_tab +[6]: ../contacts/4.creating_a_group +[7]: ../contacts/5.creating_a_smart_group +[9]: ../activities/3.creating_a_poll +[10]: ../messages/7.pending +[11]: ../activities/4.creating_an_announcement [12]: ../images/help/quick_message1.png [13]: ../images/help/quickmessage2.png [14]: ../images/help/quickmessage3.png diff --git a/plugins/frontlinesms-core/web-app/help/core/messages/4.filtering_messages.txt b/plugins/frontlinesms-core/grails-app/conf/help/messages/4.filtering_messages.txt similarity index 58% rename from plugins/frontlinesms-core/web-app/help/core/messages/4.filtering_messages.txt rename to plugins/frontlinesms-core/grails-app/conf/help/messages/4.filtering_messages.txt index 0b1d5a99a..36d42cce1 100644 --- a/plugins/frontlinesms-core/web-app/help/core/messages/4.filtering_messages.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/messages/4.filtering_messages.txt @@ -6,17 +6,20 @@ When viewing a message list anywhere in the program, you can search within that ![Filtering Messages][6] -1. At the bottom of the message list (1) you will see the above option to show All or [Starred][1]. -2. Clicking on [starred][1] will cause only starred messages to appear in the message list. -3. Clicking on All will cause all messages to be displayed. +At the bottom of the message list (1) you will see the above option to show All or [Starred][1]. Clicking on [starred][1] will cause only starred messages to appear in the message list. Clicking on All will cause all messages to be displayed. _**Note**: Navigating away from this list will cause the message list to revert to its default state of showing All messages._ +When in an activity or folder, there will be more filtering options present, allowing you to view only incoming or outgoing messages. + +![Filtering Messages][7] + ### Related Actions: [How to Mark Important Messages][1] [How to Use the Search Tab][4] -[1]: core/messages/2.sss -[4]: core/search/1.getting_around_the_search_tab -[5]: core/messages/8.mrfd +[1]: ../messages/2.sss +[4]: ../search/1.getting_around_the_search_tab +[5]: ../messages/8.mrfd [6]: ../images/help/filtering_messages.png +[7]: ../images/help/filtering2.png diff --git a/plugins/frontlinesms-core/web-app/help/core/messages/5.sent.txt b/plugins/frontlinesms-core/grails-app/conf/help/messages/5.sent.txt similarity index 85% rename from plugins/frontlinesms-core/web-app/help/core/messages/5.sent.txt rename to plugins/frontlinesms-core/grails-app/conf/help/messages/5.sent.txt index ecb1c1677..d31ab6963 100644 --- a/plugins/frontlinesms-core/web-app/help/core/messages/5.sent.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/messages/5.sent.txt @@ -35,12 +35,12 @@ The selected messages will be moved to your chosen destination. [Archiving messages][7] [Unarchiving Messages][9] -[1]: core/messages/8.mrfd -[3]: core/messages/1.getting_around_the_messages_tab -[4]: core/archive/1b.sent_archive -[6]: core/activities/1.getting_around_activities -[7]: core/archive/2.archiving_messages -[8]: core/folders/1.getting_around_folders -[9]: core/archive/4.unarchive +[1]: ../messages/8.mrfd +[3]: ../messages/1.getting_around_the_messages_tab +[4]: ../archive/1b.sent_archive +[6]: ../activities/1.getting_around_activities +[7]: ../archive/2.archiving_messages +[8]: ../folders/1.getting_around_folders +[9]: ../archive/4.unarchive [10]: ../images/help/sent.png [11]: ../images/help/messageforward.png diff --git a/plugins/frontlinesms-core/web-app/help/core/messages/6.trash.txt b/plugins/frontlinesms-core/grails-app/conf/help/messages/6.trash.txt similarity index 83% rename from plugins/frontlinesms-core/web-app/help/core/messages/6.trash.txt rename to plugins/frontlinesms-core/grails-app/conf/help/messages/6.trash.txt index dfe015dff..be504de23 100644 --- a/plugins/frontlinesms-core/web-app/help/core/messages/6.trash.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/messages/6.trash.txt @@ -23,7 +23,7 @@ All messages in the trash folder will be permanently deleted. [Moving Messages][2] [Unarchiving Messages][5] -[2]: core/messages/8.mrfd -[4]: core/archive/2.archiving_messages -[5]: core/archive/4.unarchive +[2]: ../messages/8.mrfd +[4]: ../archive/2.archiving_messages +[5]: ../archive/4.unarchive [6]: ../images/help/trash.png \ No newline at end of file diff --git a/plugins/frontlinesms-core/web-app/help/core/messages/7.pending.txt b/plugins/frontlinesms-core/grails-app/conf/help/messages/7.pending.txt similarity index 85% rename from plugins/frontlinesms-core/web-app/help/core/messages/7.pending.txt rename to plugins/frontlinesms-core/grails-app/conf/help/messages/7.pending.txt index 5fc9d6fd7..e2db9a6cc 100644 --- a/plugins/frontlinesms-core/web-app/help/core/messages/7.pending.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/messages/7.pending.txt @@ -34,11 +34,11 @@ Please see [here][2] [Unarchiving Messages][11] [Moving, Deleting, Forwarding and Replying][2] -[1]: core/messages/5.sent -[2]: core/messages/8.mrfd -[3]: core/archive/2.archiving_messages -[4]: core/messages/9.exporting -[6]: core/messages/1.getting_around_the_messages_tab -[8]: core/activities/2.creating_an_activity -[11]: core/archive/4.unarchive +[1]: ../messages/5.sent +[2]: ../messages/8.mrfd +[3]: ../archive/2.archiving_messages +[4]: ../messages/9.exporting +[6]: ../messages/1.getting_around_the_messages_tab +[8]: ../activities/2.creating_an_activity +[11]: ../archive/4.unarchive [12]: ../images/help/pending.png diff --git a/plugins/frontlinesms-core/web-app/help/core/messages/8.mrfd.txt b/plugins/frontlinesms-core/grails-app/conf/help/messages/8.mrfd.txt similarity index 91% rename from plugins/frontlinesms-core/web-app/help/core/messages/8.mrfd.txt rename to plugins/frontlinesms-core/grails-app/conf/help/messages/8.mrfd.txt index 0d948b332..5c5fe6ed8 100644 --- a/plugins/frontlinesms-core/web-app/help/core/messages/8.mrfd.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/messages/8.mrfd.txt @@ -79,14 +79,14 @@ The message will be queued to be sent. [Exporting Messages][13] [Selecting Multiple Messages][5] -[1]: core/messages/1.getting_around_the_messages_tab -[2]: core/activities/1.getting_around_activities -[3]: core/activities/1.getting_around_activities -[5]: core/messages/2.sss -[6]: core/messages/5.sent -[8]: core/messages/6.trash -[10]: core/folders/1.getting_around_folders -[13]: core/messages/9.exporting +[1]: ../messages/1.getting_around_the_messages_tab +[2]: ../activities/1.getting_around_activities +[3]: ../activities/1.getting_around_activities +[5]: ../messages/2.sss +[6]: ../messages/5.sent +[8]: ../messages/6.trash +[10]: ../folders/1.getting_around_folders +[13]: ../messages/9.exporting [14]: ../images/help/mrfd_reply.png [15]: ../images/help/mrfd_multiple.png [16]: ../images/help/mrfd_reply.png @@ -94,4 +94,4 @@ The message will be queued to be sent. [18]: ../images/help/mrfd_reply.png [19]: ../images/help/mrfd_reply.png [20]: ../images/help/activity_delete.png -[21]: ../images/help/forward.png \ No newline at end of file +[21]: ../images/help/forward.png diff --git a/plugins/frontlinesms-core/web-app/help/core/messages/9.exporting.txt b/plugins/frontlinesms-core/grails-app/conf/help/messages/9.exporting.txt similarity index 70% rename from plugins/frontlinesms-core/web-app/help/core/messages/9.exporting.txt rename to plugins/frontlinesms-core/grails-app/conf/help/messages/9.exporting.txt index a4b9ad651..c329bc287 100644 --- a/plugins/frontlinesms-core/web-app/help/core/messages/9.exporting.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/messages/9.exporting.txt @@ -7,20 +7,16 @@ Exporting lets you to extract messages from FrontlineSMS Version 2 and format th You can export messages from anywhere in the software. 1. When you are viewing the list of messages that you wish to export, click on the "Export" button. This is found in different places depending on where you are: - - [Inbox][2], [Sent][3], [Pending][4], [Inbox Archive][5], [Sent Archive][6], [Contacts][12]: core/There is a button in the header called "Export". This will export all messages in the respective folder + - [Inbox][2], [Sent][3], [Pending][4], [Inbox Archive][5], [Sent Archive][6], [Contacts][12]: ../There is a button in the header called "Export". This will export all messages in the respective folder ![Exporting From the Messages Tab][19] - - [Activities][7] and [Folders][8]: core/Export can be found in the "More Actions..." drop down. This will export all messages in the activity/folder + - [Activities][7] and [Folders][8]: ../Export can be found in the "More Actions..." drop down. This will export all messages in the activity/folder ![Exporting from an Activity or Folder][20] - - [Search][9]: core/The export button can be found in the header + - [Search][9]: ../The export button can be found in the header ![Exporting From the Search Tab][21] 2. A new window will appear prompting you to choose which format you would like to export to, and how many messages are being exported. ![Export Dialog][22] -#### Technical Tips! - -_CSV stands for Comma Separated Values. This type of file can be opened by most spreadsheet software, including Microsoft Excel. If you want to manipulate your messages in a spreadsheet format then CSV would be the most appropriate format. -PDF stands for Portable Document Format. This type of file can be opened by PDF readers such as Adobe Acrobat. If you want to print your messages then PDF is the most appropriate format._ 3. After selecting a format for your messages, click "Export". 4. You will be prompted by your browser to choose a location to save your exported messages. *__If this doesn't happen__* then the file has been automatically saved to your browsers default downloads folder. @@ -31,6 +27,7 @@ You can export the entire database of your [contacts][12], a particular [group][ 1. When viewing "All Contacts" in the [Contacts][12] tab, the export button can be found in the contact [header][1]: 2. Clicking on "Export" will cause a new window to appear prompting you to choose which format you would like to export to. +![Contact Export Dialog][25] 3. After selecting a format for your contacts, click "Export". 4. You will be prompted by your browser to choose a location to save your exported contacts. *__If this doesn't happen__* then the file has been automatically saved to your browsers default downloads folder. @@ -50,26 +47,33 @@ You can export the entire database of your [contacts][12], a particular [group][ 3. After selecting a format for your contacts, click "Export". 4. You will be prompted by your browser to choose a location to save your exported contacts. *__If this doesn't happen__* then the file has been automatically saved to your browsers default downloads folder. +#### Technical Tips! + +__CSV stands for Comma Separated Values. This type of file can be opened by most spreadsheet software, including Microsoft Excel. If you want to manipulate your messages in a spreadsheet format then CSV would be the most appropriate format. +PDF stands for Portable Document Format. This type of file can be opened by PDF readers such as Adobe Acrobat. If you want to print your messages then PDF is the most appropriate format. +The VCF format for exported contacts is useful for importing into your phone, and contact management software such as Outlook.__ + ### Related Actions [Moving, Replying, Deleting and Forwarding][17] [Archiving][18] -[1]: core/messages/9.exporting -[2]: core/messages/1.getting_around_the_messages_tab -[3]: core/messages/5.sent -[4]: core/messages/7.pending -[5]: core/archive/1a.inbox_archive -[6]: core/archive/1b.sent_archive -[7]: core/activities/1.getting_around_activities -[8]: core/folders/1.getting_around_folders -[9]: core/search/1.getting_around_the_search_tab -[12]: core/contacts/1.getting_around_the_contacts_tab -[13]: core/contacts/4.creating_a_group -[14]: core/contacts/5.creating_a_smart_group -[17]: core/messages/8.mrfd -[18]: core/archive/2.archiving_messages +[1]: ../messages/9.exporting +[2]: ../messages/1.getting_around_the_messages_tab +[3]: ../messages/5.sent +[4]: ../messages/7.pending +[5]: ../archive/1a.inbox_archive +[6]: ../archive/1b.sent_archive +[7]: ../activities/1.getting_around_activities +[8]: ../folders/1.getting_around_folders +[9]: ../search/1.getting_around_the_search_tab +[12]: ../contacts/1.getting_around_the_contacts_tab +[13]: ../contacts/4.creating_a_group +[14]: ../contacts/5.creating_a_smart_group +[17]: ../messages/8.mrfd +[18]: ../archive/2.archiving_messages [19]: ../images/help/export_message_tab.png [20]: ../images/help/export_activity.png [21]: ../images/help/export_search_tab.png [22]: ../images/help/export_dialog.png [24]: ../images/help/groupexport.png +[25]: ../images/help/export_contact.png diff --git a/plugins/frontlinesms-core/web-app/help/core/search/1.getting_around_the_search_tab.txt b/plugins/frontlinesms-core/grails-app/conf/help/search/1.getting_around_the_search_tab.txt similarity index 88% rename from plugins/frontlinesms-core/web-app/help/core/search/1.getting_around_the_search_tab.txt rename to plugins/frontlinesms-core/grails-app/conf/help/search/1.getting_around_the_search_tab.txt index a3a131393..342322d96 100644 --- a/plugins/frontlinesms-core/web-app/help/core/search/1.getting_around_the_search_tab.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/search/1.getting_around_the_search_tab.txt @@ -37,10 +37,10 @@ The search header (3) displays the parameters of the search that was last carrie [Creating a Search][6] [Exporting Your Search Results][5] -[1]: core/messages/1.getting_around_the_messages_tab -[2]: core/messages/1.getting_around_the_messages_tab -[3]: core/search/1.getting_around_the_search_tab -[4]: core/messages/3.quick_message -[5]: core/search/1.getting_around_the_search_tab -[6]: core/search/2.creating_a_search +[1]: ../messages/1.getting_around_the_messages_tab +[2]: ../messages/1.getting_around_the_messages_tab +[3]: ../search/1.getting_around_the_search_tab +[4]: ../messages/3.quick_message +[5]: ../search/1.getting_around_the_search_tab +[6]: ../search/2.creating_a_search [7]: ../images/help/search_overview.png diff --git a/plugins/frontlinesms-core/web-app/help/core/search/2.creating_a_search.txt b/plugins/frontlinesms-core/grails-app/conf/help/search/2.creating_a_search.txt similarity index 83% rename from plugins/frontlinesms-core/web-app/help/core/search/2.creating_a_search.txt rename to plugins/frontlinesms-core/grails-app/conf/help/search/2.creating_a_search.txt index 181e54d6a..0772b0e8b 100644 --- a/plugins/frontlinesms-core/web-app/help/core/search/2.creating_a_search.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/search/2.creating_a_search.txt @@ -23,9 +23,9 @@ The results of your search will then be displayed in the message list (2). [Exporting Search Results][4] [Search within contact][5] -[1]: core/search/1.getting_around_the_search_tab -[2]: core/search/1.getting_around_the_search_tab -[3]: core/search/1.getting_around_the_search_tab -[4]: core/search/1.getting_around_the_search_tab -[5]: core/search/2.creating_a_search +[1]: ../search/1.getting_around_the_search_tab +[2]: ../search/1.getting_around_the_search_tab +[3]: ../search/1.getting_around_the_search_tab +[4]: ../search/1.getting_around_the_search_tab +[5]: ../search/2.creating_a_search [6]: ../images/help/basic_search.png diff --git a/plugins/frontlinesms-core/web-app/help/core/search/2a.creating_an_advanced_search.txt b/plugins/frontlinesms-core/grails-app/conf/help/search/2a.creating_an_advanced_search.txt similarity index 93% rename from plugins/frontlinesms-core/web-app/help/core/search/2a.creating_an_advanced_search.txt rename to plugins/frontlinesms-core/grails-app/conf/help/search/2a.creating_an_advanced_search.txt index d175bd568..ed023f1eb 100644 --- a/plugins/frontlinesms-core/web-app/help/core/search/2a.creating_an_advanced_search.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/search/2a.creating_an_advanced_search.txt @@ -77,12 +77,12 @@ In this example search, we will be searching for received messages with the key [Exporting Search Results][1] [Search within contact][7] -[1]: core/search/1.getting_around_the_search_tab -[2]: core/search/2a.creating_an_advanced_search -[3]: core/archive/1.getting_around_the_archive_tab -[4]: core/search/1.getting_around_the_search_tab -[5]: core/search/2.creating_a_search -[6]: core/search/1.getting_around_the_search_tab -[7]: core/contacts/8.searching_through_contacts -[8]: core/search/1.getting_around_the_search_tab +[1]: ../search/1.getting_around_the_search_tab +[2]: ../search/2a.creating_an_advanced_search +[3]: ../archive/1.getting_around_the_archive_tab +[4]: ../search/1.getting_around_the_search_tab +[5]: ../search/2.creating_a_search +[6]: ../search/1.getting_around_the_search_tab +[7]: ../contacts/8.searching_through_contacts +[8]: ../search/1.getting_around_the_search_tab [9]: ../images/help/advanced_search.png diff --git a/plugins/frontlinesms-core/web-app/help/core/settings/1.getting_around_the_settings_menu.txt b/plugins/frontlinesms-core/grails-app/conf/help/settings/1.getting_around_the_settings_menu.txt similarity index 52% rename from plugins/frontlinesms-core/web-app/help/core/settings/1.getting_around_the_settings_menu.txt rename to plugins/frontlinesms-core/grails-app/conf/help/settings/1.getting_around_the_settings_menu.txt index 7145926de..07c8411b9 100644 --- a/plugins/frontlinesms-core/web-app/help/core/settings/1.getting_around_the_settings_menu.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/settings/1.getting_around_the_settings_menu.txt @@ -1,6 +1,6 @@ # Getting Around the Settings Menu -The settings menu is where you can [configure the language][1] that FrontlineSMS runs in, [restore a backup][2] and [configure the connections to your detected devices][3]. You can get to the settings menu by clicking on "Settings" in the top right of your screen, next to "Help". +The settings menu is where you can [configure the language][1] that FrontlineSMS runs in, [restore a backup][2], [configure the connections to your detected devices][3] and [enable password protection][7]. You can get to the settings menu by clicking on "Settings" in the top right of your screen, next to "Help". ![Settings Menu][6] @@ -12,9 +12,10 @@ There are 3 sections in the settings area. Please select one to see more informa **[Phones and Connections][3]**: Settings for connections and devices can be found here. **[System][5]**: You can view the system logs from here and choose to export them. -[1]: core/settings/2.changing_languages -[2]: core/settings/3.restoring_a_backup -[3]: core/settings/2.changing_languages -[4]: core/settings/2.changing_languages -[5]: core/settings/2.changing_languages +[1]: ../settings/2.changing_languages +[2]: ../settings/3.restoring_a_backup +[3]: ../settings/2.changing_languages +[4]: ../settings/2.changing_languages +[5]: ../settings/2.changing_languages +[7]: ../settings/8.basic_auth [6]: ../images/help/settings_menu.png diff --git a/plugins/frontlinesms-core/grails-app/conf/help/settings/10.custom_routing_rules.txt b/plugins/frontlinesms-core/grails-app/conf/help/settings/10.custom_routing_rules.txt new file mode 100644 index 000000000..551f78ca5 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/conf/help/settings/10.custom_routing_rules.txt @@ -0,0 +1,54 @@ +# Creating Rules for Sending Messages Through Multiple Phone Numbers or Connections + +FrontlineSMS is highly configurable, meaning many features in the application will help you send messages in a useful way. One of the most powerful tools for this is configuring routing rules, in which you can select how to messages through a variety of your phone numbers. + +If you only have a single connection, such as through one modem, then messages will always send and receive through that single connection. But once you add an additional connection, such as an additional modem or an Android phone using SMSSync, FrontlineSMS will allow you to create rules for sending outgoing messages. + +In other words, routing rules allow you to set your own preferences for sending messages, and improve the chances that your messages will be delivered to the people you are trying to reach. + +This help file explains how to create these routing rules. + +If you would like to set routing rules, click "Settings" in the upper right-hand corner of your screen, then "Phones and Connections." Your screen should now look something like the one below (although perhaps with fewer connections than our test account): + +![Connection routing rules][1] + +Now you can create routing rules following the steps below. + +1. First, make sure you have all of your connections configured. This could be multiple modems, a modem and an instance of SMSSync, or a combination of these. + +You will then see a list of options, as in the screenshot above. + +2. Next, decide whether outgoing messages should be sent through the most recent number used by the people you are trying to reach. + +In many cases, the people you are trying to reach will message the number they are most familiar with. Therefore, you can instruct FrontlineSMS to send outgoing messages through that same number. + +For instance, if Jane is trying to reach you and has your Android telephone number, you probably want to respond to Jane through that same number. If you respond through a different phone number, she may not know who you are! To always reply to someone through the same number they messaged most recently, select the very first box, as indicated in the screenshot below. + +![Connection routing rules 2][2] + +3. Now decide the priority order for other connections in your list. + +You can also set the priority order for the other phone numbers you have connections with. Note that you can do this whether or not you have selected the box to send messages through the most recent number contacts have messaged. This is useful for: + +- situations in which you are messaging contacts for the first time, because you can prioritize the least expensive or most reliable phone number +- forcing FrontlineSMS to use your preferred phone number, no matter how other contacts have reached you +- prioritizing how FrontlineSMS will message someone if a phone number fails; in other words, if FrontlineSMS cannot reach someone through your first preferred connection, it will try again on the second most preferred connection, and so on + +In order to create a prioritized list, you can 'drag and drop' your phone numbers to create your preferred order. Below is one example. Let's say the third connection on your list is actually the one you prefer to use for sending most messages. You can 'grab' the number by hovering over the left-hand tab with your cursor, where the arrow is pointing in the screenshot below. + +4. Finally, be sure to deselect any phone numbers you do not want FrontlineSMS to use. + +In the event that you do NOT want FrontlineSMS to use a certain phone number, you can simply deselect it in the list by clicking on the relevant box and ensuring the checkmark disappears. + +**Note: remember to save your selection!** + +## Related topics: +![Manually Adding A Device][3] +![SMSSync][4] + + +[1]: ../images/help/ConnectionsRoutingRules.png +[2]: ../images/help/ConnectionsRoutingRules2.png +[3]: ../settings/5.manually_adding_device +[4]: ../settings/9.smssync + diff --git a/plugins/frontlinesms-core/grails-app/conf/help/settings/11.contact_import.txt b/plugins/frontlinesms-core/grails-app/conf/help/settings/11.contact_import.txt new file mode 100644 index 000000000..f11c773d7 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/conf/help/settings/11.contact_import.txt @@ -0,0 +1,33 @@ +## Importing Multiple Contacts + +If you have a large number of contacts to import, then you can use a different mechanism for importing them into FrontlineSMS by following the instructions below. The important steps are highlighted in the screenshot below. + +![Importing Multiple Contacts][1] + +1. Make sure your column headings are correct. + +The column headings should follow the example in the screenshot below. Note that "group" is optional; it will be interpreted by FrontlineSMS as a group tag for the contact. + +**Note: the column headings must be exact (e.g. 'E-mail Address', not 'email address').** + +![Importing Multiple Contacts Headings][2] + +**Note: If you are importing using vCards, you only need to ensure that your contact's mobile number is correctly listed as a mobile number; FrontlineSMS will ignore other numbers.** + +2. Click on 'Settings' in the upper right-hand corner of the window. Then click on 'Import and Export.' + +3. Select the type of file you wish to import. You can choose .csv - a common database format, or a vCard / .vcf format which is used in many contact management systems. + +4. Select the file from your computer. If it is a vCard it will import at this point. If it is a .csv file, you will now have the chance to review your file, as per the screenshot below. Ensure that your column headings are accurate; they will appear as green if they are correct, or yellow if they are incorrect. Once you're happy with the column headings, click "Import all contacts." FrontlineSMS will tell you if your contacts imported successfully or now. + +![Review Contact Import][3] + +If your contact did not import successfully, you will receive a fail notice as pictured below. FrontlineSMS will ask if you want to download a spreadsheet of the failed contact entries so that you can correct the issues and attempt to import them again into FrontlineSMS. + +![Import Fail Notice][4] + +[1]: ../images/help/ContactImport.png +[2]: ../images/help/contact_import/Screenshot_5_20_13_4_24_PM.jpg +[3]: ../images/help/contact_import/ReviewContactImport.png +[4]: ../images/help/contact_import/ImportFailNotice.png + diff --git a/plugins/frontlinesms-core/web-app/help/core/settings/2.changing_languages.txt b/plugins/frontlinesms-core/grails-app/conf/help/settings/2.changing_languages.txt similarity index 81% rename from plugins/frontlinesms-core/web-app/help/core/settings/2.changing_languages.txt rename to plugins/frontlinesms-core/grails-app/conf/help/settings/2.changing_languages.txt index a66ca21be..49ff4a6de 100644 --- a/plugins/frontlinesms-core/web-app/help/core/settings/2.changing_languages.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/settings/2.changing_languages.txt @@ -15,7 +15,7 @@ FrontlineSMS will change to the language you have specified. [Importing Contacts and Messages][2] [Configuring Detected Devices][3] -[1]: core/settings/2.changing_languages -[2]: core/settings/3.restoring_a_backup -[3]: core/settings/1.getting_around_the_settings_menu +[1]: ../settings/2.changing_languages +[2]: ../settings/3.restoring_a_backup +[3]: ../settings/1.getting_around_the_settings_menu [4]: ../images/help/settings_language.png diff --git a/plugins/frontlinesms-core/web-app/help/core/settings/3.restoring_a_backup.txt b/plugins/frontlinesms-core/grails-app/conf/help/settings/3.restoring_a_backup.txt similarity index 88% rename from plugins/frontlinesms-core/web-app/help/core/settings/3.restoring_a_backup.txt rename to plugins/frontlinesms-core/grails-app/conf/help/settings/3.restoring_a_backup.txt index 026059a02..479fa0f2e 100644 --- a/plugins/frontlinesms-core/web-app/help/core/settings/3.restoring_a_backup.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/settings/3.restoring_a_backup.txt @@ -17,7 +17,7 @@ If the import was unsuccessful, or some contacts or messages failed to import, a [Changing languages][2] [Configuring Detected Devices][3] -[1]: core/settings/3.restoring_a_backup -[2]: core/settings/2.changing_languages -[3]: core/settings/3.restoring_a_backup +[1]: ../settings/3.restoring_a_backup +[2]: ../settings/2.changing_languages +[3]: ../settings/3.restoring_a_backup [4]: ../images/help/settings_import.png diff --git a/plugins/frontlinesms-core/grails-app/conf/help/settings/4.setting_up_a_device.txt b/plugins/frontlinesms-core/grails-app/conf/help/settings/4.setting_up_a_device.txt new file mode 100644 index 000000000..e37136f87 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/conf/help/settings/4.setting_up_a_device.txt @@ -0,0 +1,57 @@ +# Setting up a Modem or Phone + +_Before you can start using FrontlineSMS Version 2, you will have to connect and configure a device to send and receive messages. Some of the work is done automatically by FrontlineSMS to make it easier for you. If you are having trouble setting up a phone or modem, or want step-by-step instructions to get started, then this page will explain how to connect and configure a phone or modem in FrontlineSMS. The detection process differs if you have already attached your device to your computer before running FrontlineSMS and when you attach a device while FrontlineSMS is running._ + +### Detecting a Phone or a Modem + +FrontlineSMS Version 2 can automatically connect to your phone or modem, provided that it can gather specific information. We call this [device detection][2]. You can follow the steps below to ensure your device is connected properly. + +1. If it's already open, close the FrontlineSMS software. This is to make sure your phone or modem is connecting correctly to your computer, before connecting it to our application. + +2. Once FrontlineSMS is closed, make sure your phone or modem is physically connected to your computer. + +3. Make sure that the driver software is installed. For newer Windows and Mac machines, the drivers should install automatically when you attach your phone or modem to your computer for the first time. If you don't know how to do this, please consult your device's operating instructions. + +4. Once your device is attached and the device's drivers have been installed, please start FrontlineSMS Version 2. + +5. FrontlineSMS Version 2 will automatically detect the attached device and begin to collate the information it needs to create a usable connection. + +**_Note:** This process can take a few **minutes**. Please be patient and allow the software time to work. You will be notified if the connection has failed or if no device is detected. If the connection fails then click [here][3] to manually set up a connection. + +![Connecting][10] + +6. After a few minutes, a notification should appear informing you that a successful connection has been established to your device and it can now be used for sending and receiving messages. + +![Successfully Connected][9] + +This will also be indicated by a green light on the [status tab][2]. The connection will also appear in the [connections][2] area of the [status tab][2]. + +### Attaching a Device While FrontlineSMS Version 2 is Running + +If you start FrontlineSMS Version 2 and then attach a phone or modem to your computer, you will have to prompt FrontlineSMS to scan your system so that it can begin the automatic device setup process. This approach is only recommended if your phone or modem has already been connected to FrontlineSMS previously. + +As noted above, make sure the device's drivers are installed and that the device is correctly detected by your operating system before continuing with these steps. + +1. Navigate to the [status tab][2] and click on "Detect Modems". +This will begin the automatic set up process. +2. You should see activity in the "Detected Devices" area. The process from this point is the same as above. +**_Note:** This process can take a few **minutes**. Please be patient and allow the software time to work. You will be notified if the connection has failed or if no device is detected. If the connection fails then click [here][3] to manually set up a connection. If no device is detected, please click [here][3] to see what you can do to try and fix the issue._ + +![Connecting][10] + +You will be notified once your connection has been set up successfully +![Successfully Connected][9] + +If you are having trouble getting a connection set up then please click [here][3] to try and manually set up a connection. + +--- + +### Related Actions +[Manually Setting Up A Connection][3] +[Editing a Connection][6] + +[2]: ../status/1.getting_around_the_status_tab +[3]: ../settings/5.manually_adding_device +[6]: ../settings/6.edit_delete_connection +[9]: ../images/help/successfully_connected.png +[10]: ../images/help/connecting.png diff --git a/plugins/frontlinesms-core/web-app/help/core/settings/4a.intellisms.txt b/plugins/frontlinesms-core/grails-app/conf/help/settings/4a.intellisms.txt similarity index 90% rename from plugins/frontlinesms-core/web-app/help/core/settings/4a.intellisms.txt rename to plugins/frontlinesms-core/grails-app/conf/help/settings/4a.intellisms.txt index b1b50d05e..a4b6e470e 100644 --- a/plugins/frontlinesms-core/web-app/help/core/settings/4a.intellisms.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/settings/4a.intellisms.txt @@ -36,11 +36,11 @@ If the connection fails then you may have entered information incorrectly. To ma [Editing a Connection][3] [Setting up a Connection to a Clickatell Account][6] -[2]: core/settings/1.getting_around_the_settings_menu -[3]: core/settings/6.edit_delete_connection -[4]: core/http://frontlinesms.ning.com/ -[5]: core/settings/5.manually_adding_device -[6]: core/settings/4b.clickatell +[2]: ../settings/1.getting_around_the_settings_menu +[3]: ../settings/6.edit_delete_connection +[4]: ../settings/http://frontlinesms.ning.com/ +[5]: ../settings/5.manually_adding_device +[6]: ../settings/4b.clickatell [7]: ../images/help/settings_connections.png [8]: ../images/help/add_new_connection.png [9]: ../images/help/intellisms.png diff --git a/plugins/frontlinesms-core/web-app/help/core/settings/4b.clickatell.txt b/plugins/frontlinesms-core/grails-app/conf/help/settings/4b.clickatell.txt similarity index 81% rename from plugins/frontlinesms-core/web-app/help/core/settings/4b.clickatell.txt rename to plugins/frontlinesms-core/grails-app/conf/help/settings/4b.clickatell.txt index a332892d8..6b02c154a 100644 --- a/plugins/frontlinesms-core/web-app/help/core/settings/4b.clickatell.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/settings/4b.clickatell.txt @@ -14,7 +14,8 @@ Please have your Clickatell account details to hand. You will need your Clickate ![Add New Connection][8] 3. Select "Clickatell Account" and click "Next" -4. On this screen, please enter the required information. +4. On this screen, please enter the required information. +5. If you are sending messages to USA then you may consider filling in the fields 'Send To USA' and 'From Number' ![Clickatell Screen][9] @@ -34,11 +35,11 @@ If the connection fails then you may have entered information incorrectly. To ma [Editing a Connection][3] [Setting up a Connection to an IntelliSMS Account][6] -[2]: core/settings/1.getting_around_the_settings_menu -[3]: core/settings/6.edit_delete_connection -[4]: core/http://frontlinesms.ning.com/ -[5]: core/settings/5.manually_adding_device -[6]: core/settings/4a.intellisms +[2]: ../settings/1.getting_around_the_settings_menu +[3]: ../settings/6.edit_delete_connection +[4]: ../settings/http://frontlinesms.ning.com/ +[5]: ../settings/5.manually_adding_device +[6]: ../settings/4a.intellisms [7]: ../images/help/settings_connections.png [8]: ../images/help/add_new_connection.png [9]: ../images/help/clickatell.png diff --git a/plugins/frontlinesms-core/grails-app/conf/help/settings/4c.connecting_to_frontlinesync.txt b/plugins/frontlinesms-core/grails-app/conf/help/settings/4c.connecting_to_frontlinesync.txt new file mode 100644 index 000000000..4a388886b --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/conf/help/settings/4c.connecting_to_frontlinesync.txt @@ -0,0 +1,72 @@ +# Creating a FrontlineSync Connection on FrontlineSMS + +FrontlineSync is an Android app which allows users to connect their device to FrontlineSMS in order to use the device as a connection for sending and receiving SMSs and tracking missed calls. Please note you will need FrontlineSMS in addition to FrontlineSync in order to use this service. + +FrontlineSync does not directly limit the number of messages that can be sent via the app, however, your Android device or mobile service provider may limit the number of messages that can be sent during a given period. + +# Setting up a new FrontlineSync Connection on FrontlineSMS + +Enabling the use of FrontlineSync as an SMS and missed call gateway is a 2 step process; you must create a FrontlineSync connection in your FrontlineSMS workspace, then you will need to download and install the FrontlineSync Android app on your Android through the Play Store. Information from your new connection will then need to be entered into the Android app: + +# Step 1, creating your connection in FrontlineSMS + +Navigate to the connections tab in FrontlineSMS and click on the 'Add new connection' button. + +![Add-New-Connection][1] + +A list of the available connections will appear as below. Select FrontlineSync and click 'Next'. + +![Select connection][2] +This will create a new connection by default named 'FrontlineSync'. Click 'Next' unless you wish to set a particular name for the connection. + +![connection name][3] +You can ignore the next screen and click 'Create' to complete the set up as the page simply confirms your options + +Your new connection will now appear on your Connections page. The connection will have be set to 'Connecting status (Orange light)' while it waits to complete the connection to the Android App. + +![Connecting state][4] +Your work on FrontlineSMS is now done and you now need to enter details into the Android app. + +#Step 2: Installing FrontlineSync and connecting the app + +If not downloaded already, please visit the Google Play Store and download FrontlineSync onto your Android. FrontlineSync is compatible with Android versions 2.3.0 and above. After completing the installation, open the application to configure your connection: + +![Select type][5] + +Select the the Frontline product which you would like to link FrontlineSync to, in this case, FrontlineSMS +Enter your Android Identifier and Passcode which were generated in the final step of setting up your new connection in your FrontlineSMS workspace. You will also need to enter the address of your FrontlineSMS computer; [see further details on how to get this address][10]. +You can define a port number of your choice in this step, however the application will append port 8130 by default if not set. The protocol expected is http:// and this will also be auto-completed if missing. + +![Connect with passcode][6] + +The app will now try to contact your FrontlineSMS workspace. It will only proceed to Step 3 if successful. Progressing from Step 2/4 will also auto the connection status in FrontlineSMS from Orange to Green. +Once a connection has been reached you need to set the data that you would like FrontlineSync to handle. Here you can also set the frequency that the FrontlineSync will communicate with FrontlineSMS; a lower number here will mean FrontlineSync checks with FrontlineSMS more regularly but can affect your Android's battery life. + +You are all set! Next you will be taken to the app's dashboard. See more details on the dashboard. + +![Options][7] + +If you now return to FrontlineSMS, you will now see that the FrontlineSync connection status is now green showing that there is communication with your Android phone. If you expand the connection options link, you will see that the Options you chose in set up match what you set up on the Android. +![green connected][8] + +# Troubleshooting tips +If you notice that your FrontlineSync connection is not sending or receiving messages and uploading missed calls: +* Check that the application on your Android app has not been switched off. +* Check that you have configured your sync settings. +* Ensure that you have got enough mobile phone credit and that the phone is connected to Internet. +* Ensure that the connection is not disabled on FrontlineSMS. +* Check that the Connection is placed first on top of your routing rules. +* The sending limit for bulk messages depends on your Android version. FrontlineSMS alerts you when you exceed the limit and when you click deny the items will remain in the pending messages folder. + +![SMS exceeded][9] + +[1]: ../images/help/frontlinesync/1.1.Add-New-Connection.jpg +[2]: ../images/help/frontlinesync/1.2.Select-Modem-Type.jpg +[3]: ../images/help/frontlinesync/1.3.Connection-Name.jpg +[4]: ../images/help/frontlinesync/1.4.orange_connection.jpg +[5]: ../images/help/frontlinesync/2.1.config.jpg +[6]: ../images/help/frontlinesync/2.2.passcode_and_ip.jpg +[7]: ../images/help/frontlinesync/2.3.options.jpg +[8]: ../images/help/frontlinesync/3.1.green_connection.jpg +[9]: ../images/help/frontlinesync/3.2.too_many_sms.jpg +[10]: settings/4d.finding_on_a_lan diff --git a/plugins/frontlinesms-core/grails-app/conf/help/settings/4d.finding_on_a_lan.txt b/plugins/frontlinesms-core/grails-app/conf/help/settings/4d.finding_on_a_lan.txt new file mode 100644 index 000000000..abc504789 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/conf/help/settings/4d.finding_on_a_lan.txt @@ -0,0 +1,39 @@ +# Connecting FrontlineSync to FrontlineSMS on a local network + +A smartphone running FrontlineSync can communicate with your computer running FrontlineSMS over an existing local network (LAN), via smartphone tethering, or through a public-facing IP over the Internet. + +#RECOMMENDED: Connecting FrontlineSMS and FrontlineSync over an existing local network (LAN). + +1. Ensure that your computer running FrontlineSMS is connected to your local network, your LAN is probably handled using by a router. + +2. Connect your Android smartphone running FrontlineSync to your local network via WiFi. + +3. Look up your computer’s local IP address. (There is a useful document written by LinkSys on how to find this) + +4. On your Android phone, open FrontlineSync and select 'Connect to FrontlineSMS'. In Step 2 of 4 of the FrontlineSync configuration menu, you will need to enter the IP address of the computer running FrontlineSMS in the third field after entering the auto-generated ID and Passcode. + +5. Please note that your router may assign a new IP address to your FrontlineSMS machine every so often, or if your machine reconnects to the local network. If this occurs, you will need to repeat steps 3 and 4 to check the IP hasn't changed. If it has changed it is a simple update that only needs to be done on the FrontlineSync connection configuration settings on your Android. +It is possible to configure your router to assign the same local IP address to your machine each time. Contact your network administrator or consult your router’s manual for more details. + +# Connecting FrontlineSMS and FrontlineSync via smartphone tethering. + +1. Set up your smartphone as a wireless hotspot or source a connecting USB cable. (Note: Some carriers may restrict this functionality or require an additional fee to enable it.) + +2. Connect your computer running FrontlineSMS to the smartphone-created WiFi network or via the cable. (Note that your computer’s internet connection will transfer from WiFi to your smartphone’s data connection if using the hotspot route.) + +3. Look up your computer’s local IP address. (There is a useful document written by LinkSys on how to find this) + +4. On your Android phone, open FrontlineSync and select 'Connect to FrontlineSMS'. In Step 2 of 4 of the FrontlineSync configuration menu, you will need to enter the IP address of the computer running FrontlineSMS in the third field after entering the auto-generated ID and Passcode. + +#ADVANCED USERS: Connecting FrontlineSMS and FrontlineSync via public IP + +1. If your computer running FrontlineSMS is accessible over the Internet via a public IP address (or a DNS service), you can connect to it with any Internet-connected smartphone running FrontlineSync wherever located. Consider contacting your network administrator if you wish to take advantage of this setup. + +2. Connect your smartphone running FrontlineSync to the Internet. + +3. Look up the public IP address you wish to use. Google will automatically report your public IP address if you search for “what is my IP”. [This Google search shows useful results]. You will probably need to set this up manually with network administration and/or you FrontlineSMS computer's DNS settings. + +4. You will probably need to set up ‘port forwarding’ on your network’s router to forward to the host computer. FrontlineSMS uses port 8130 by default so if you want to use the public IP on a different port it is easiest to forward port 8130 to your required port. 8130 will be the internal port whereas the service port will be your new chosen port. + +5. On your Android phone, open FrontlineSync and select 'Connect to FrontlineSMS'. In Step 2 of 4 of the FrontlineSync configuration menu, you will need to enter the IP address of the computer running FrontlineSMS in the third field after entering the auto-generated ID and Passcode. + diff --git a/plugins/frontlinesms-core/web-app/help/core/settings/5.manually_adding_device.txt b/plugins/frontlinesms-core/grails-app/conf/help/settings/5.manually_adding_device.txt similarity index 87% rename from plugins/frontlinesms-core/web-app/help/core/settings/5.manually_adding_device.txt rename to plugins/frontlinesms-core/grails-app/conf/help/settings/5.manually_adding_device.txt index 59d1674a0..f7c70eba5 100644 --- a/plugins/frontlinesms-core/web-app/help/core/settings/5.manually_adding_device.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/settings/5.manually_adding_device.txt @@ -55,17 +55,13 @@ If the connection fails then you may have entered information incorrectly. This ### Related Actions [Automatically Setting Up a Connection][7] -[Editing a Connection][5] -[Setting up a Connection to a Clickatell Account][8] -[Setting up a Connection to an IntelliSMS Account][9] - -[2]: core/settings/1.getting_around_the_settings_menu -[3]: core/status/1.getting_around_the_status_tab -[5]: core/settings/6.edit_delete_connection -[6]: core/http://frontlinesms.ning.com/ -[7]: core/settings/4.setting_up_a_device -[8]: core/settings/4a.intellisms -[9]: core/settings/4b.clickatell +[Editing a Connection][5] + +[2]: ../settings/1.getting_around_the_settings_menu +[3]: ../status/1.getting_around_the_status_tab +[5]: ../settings/6.edit_delete_connection +[6]: ../settings/http://frontlinesms.ning.com/ +[7]: ../settings/4.setting_up_a_device [10]: ../images/help/detect_activity.png [11]: ../images/help/settings_connections.png [12]: ../images/help/add_new_connection.png diff --git a/plugins/frontlinesms-core/web-app/help/core/settings/6.edit_delete_connection.txt b/plugins/frontlinesms-core/grails-app/conf/help/settings/6.edit_delete_connection.txt similarity index 85% rename from plugins/frontlinesms-core/web-app/help/core/settings/6.edit_delete_connection.txt rename to plugins/frontlinesms-core/grails-app/conf/help/settings/6.edit_delete_connection.txt index 776dcc49f..60536d438 100644 --- a/plugins/frontlinesms-core/web-app/help/core/settings/6.edit_delete_connection.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/settings/6.edit_delete_connection.txt @@ -39,16 +39,12 @@ The connection will be deleted. If this was your active connection then you will --- ### Related Actions -[Automatically Setting Up a Connection][1] -[Setting up a Connection to a Clickatell Account][5] -[Setting up a Connection to an IntelliSMS Account][6] +[Automatically Setting Up a Connection][1] [Manually Setting up a Connection][7] -[1]: core/settings/4.setting_up_a_device -[3]: core/settings/1.getting_around_the_settings_menu -[5]: core/settings/4b.clickatell -[6]: core/settings/4a.intellisms -[7]: core/settings/5.manually_adding_device +[1]: ../settings/4.setting_up_a_device +[3]: ../settings/1.getting_around_the_settings_menu +[7]: ../settings/5.manually_adding_device [8]: ../images/help/settings_connections.png [9]: ../images/help/edit_connection.png [10]: ../images/help/phone_modem_confirm.png diff --git a/plugins/frontlinesms-core/grails-app/conf/help/settings/7.translatingfrontlinesms.txt b/plugins/frontlinesms-core/grails-app/conf/help/settings/7.translatingfrontlinesms.txt new file mode 100644 index 000000000..181756f42 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/conf/help/settings/7.translatingfrontlinesms.txt @@ -0,0 +1,18 @@ +# Translating FrontlineSMS Version 2 + +_FrontlineSMS Version 2 offers the opportunity to translate the interface into a language that isn't currently supported. Your translation will then be made available in all future revisions of the software for everyone to use._ + +All Frontline products work with a user-friendly, internet-based application for internationalizing, or translating, the platform. The application allows you to translate the core language file of FrontlineSMS line-by-line, replacing the English text with equivalent text from the language or dialect of your choice. + +As such, the process demands an internet connection and some familiarity with computers. + +We welcome volunteers to translate our applications into any language or dialect, and we will make the translations available to others who speak the same language or dialect as you. In other words, all of your work in providing a translation will benefit everyone in the Frontline community. + +If you are interested in translating FrontlineSMS, please get in touch with us directly at support@frontlinesms.com + +### See Also +[Changing languages][1] +[Configuring Detected Devices][2] + +[1]: ../settings/2.changing_languages +[2]: ../settings/4.setting_up_a_device diff --git a/plugins/frontlinesms-core/grails-app/conf/help/settings/8.basic_auth.txt b/plugins/frontlinesms-core/grails-app/conf/help/settings/8.basic_auth.txt new file mode 100644 index 000000000..3e8cf75ae --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/conf/help/settings/8.basic_auth.txt @@ -0,0 +1,15 @@ +# Basic Authentication (password protection) + +FrontlineSMS lets you protect your data by setting a global password. Users will need to enter this password whenever they access FrontlineSMS. + +![Basic auth][1] + +## Enabling basic Authentication + +Navigate to the General Settings menu, and scroll down to the Basic Authentication section and check the 'Enable Basic Authentication' checkbox. You will be prompted to enter a username and password. Once this is done, click Save. Your browser window will immediately prompt you to log in, and will continue to do this whenever you return to FrontlineSMS. + +## Disabling basic Authentication + +To switch this feature off, you first need to be signed in to your FrontlineSMS instance using the password that was previously set up. Navigate to General Settings and uncheck the 'Enable Basic Authentication' checkbox and click save. Once this is done, you will no longer need to enter a password to use FrontlineSMS + +[1]: ../images/help/settings_basicauth.png diff --git a/plugins/frontlinesms-core/grails-app/conf/help/settings/9.smssync.txt b/plugins/frontlinesms-core/grails-app/conf/help/settings/9.smssync.txt new file mode 100644 index 000000000..14d814f71 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/conf/help/settings/9.smssync.txt @@ -0,0 +1,116 @@ +# Installing SMSSync + +SMSSync allows you to receive texts on your Android phone, and then forward those messages to a website or an application like FrontlineSMS. You will need an Android phone, and that phone must have a cellular network connection, as well as a data connection, for SMSSync to work properly. + +To install SMSSync, first, visit an Android app store and download the app: + +![SMSSync banner][1] + +When you start the app, you will see a generally blank screen like this: + +![No Sync URL available][2] + +Next, you can tap "Sync URL" and then the "+" icon to add a new URL connection. Your screen will look something like this: + +![Add Sync URL][3] + +If you need additional assistance in configuring the app, you can also visit the help page at Ushahidi's website: http://smssync.ushahidi.com/howto + +# Connecting SMSSync to FrontlineSMS Version 2 + +SMSSync is an Android application that allows your Android phone to exchange text messages with an application like FrontlineSMS. You can learn more about the product, as well as how to download it, by reading Installing SMSSync. You can also find additional assistance at Ushahidi's help site, http://smssync.ushahidi.com/howto + +In order to connect FrontlineSMS to SMSSync, you will need both applications running simultaneously. This is because you need to go through some basic steps to create the connection. After this initial setup, you do not need both applications to be running simultaneously for the connection to work properly. + +Once you've downloaded and installed SMSSync, log into FrontlineSMS and click on **Settings** in the upper right-hand corner. From there, you can click on "Connections." + +![Settings page screenshot][4] + +Select "SMSSync" and then click "Next." + +![Connection selector][5] + +Now you will see a lovely set of instructions on how to create a connection with SMSSync. Here, we expand on those a bit, including screenshots. + +![SMSSync instructions][6] + +First, enter a Secret into FrontlineSMS. This is effectively a password of your choosing that allows you your Android to verify your connection to FrontlineSMS. + +Timeout is an advanced feature that allows you to set the number of minutes your Android will attempt to sync messages before failing. In other words, once SMSSync and FrontlineSMS are connected, your Android will attempt to forward and receive messages to and from FrontlineSMS. If for some reason SMSSync cannot contact FrontlineSMS (for instance, of our servers were down), the Android phone will eventually "timeout" and stop trying to send or receive those messages. You can set a time period for how long the Android attempts to establish a connection. The default is set at 60 minutes, and if you aren't sure, you can just leave this number for now. + +You then need to name the connection. You can choose any name you like, such as "Bob's Work Android." Enter that in the final field, and click "next." + +Here is one example of how we completed the form: + +![Completed SMSSync details][7] + +If you have completed all of the fields, you will be prompted to confirm your settings. Your screen will look something like this: + +![SMSSync settings confirmation screen][8] + +If everything looks correct, then click "Done." + +Finally -- and this is very important -- make note of the URL (which is a fancy way to say web address) that appears under your connection. This is your Sync URL, and you will need it to finalize a connection to your Android phone. The Sync URL is highlighted with a purple bar in the screenshot below. + +![Sync URL screenshot][9] + +Now, on your Android phone, open up SMSSync. Tap on the "Sync URL" tab. Then you can click the "+" symbol to add a new connection. You should get a screen that looks like this: + +![Screenshot for adding Sync URL to SMSSync][10] + +The first field, "Enter Title for the Sync URL," can be anything you want. For example, "FrontlineSMS Demo". + +Next, enter the Secret Key you created in FrontlineSMS. + +You can ignore the keywords feature for now; FrontlineSMS will help you manage Keywords. + +Finally, you need to enter the URL (web address) you saved from earlier in this process. + +Your completed form will look something like this: + +![Sync URL filled][11] + +Tap "ok" to save the new entry. + +Next, you will see a screen in which you can select a Sync URL for SMSSync to connect with. Select the URL you just created. In our example, it is "FrontlineSMS Demo" Then select "Start SMSSync service." + +Your screen will look something like this: + +![SMSSync Sync URL selected][12] + +Ok, you're nearly finished. + +Finally, you need to enable "Auto Sync" and "Task Checking" within the SMSSync app settings. By default, SMSSync only checks for outgoing messages when delivering incoming ones. This means your messages from FrontlineSMS could be stuck in 'pending' for a long time with the default settings. Enabling these settings in the SMSSync app allows SMSSync to check for outgoing messages even when no incoming messages have yet arrived, as well as sync with FrontlineSMS on a regular basis. + +In order to do this, first click on "Settings" within the SMSSync app. + +![Photo of a phone running SMSSync with pull up menu][13] + +Then, scroll down and then click "Enable Auto Sync" and "Enable Task Checking." The first allows the Android to regularly send incoming messages to FrontlineSMS. The second allows the Android to check with FrontlineSMS to see if there are any messages pending. + +![Screenshot of SMSSync sync and task features][14] + +You can also set the Frequency for each task. We recommend somewhere between 1 and 30 minutes between each task depending on how quickly you need your messages sent and received, and your required battery life. + +Note: the more frequently SMSSync checks for messages, the faster it will drain your battery. We recommend leaving your Android connected to a good power supply if you need it to connect to FrontlineSMS frequently. + +![SMSSync polling frequency selection][15] + +You're finished! You can try sending a test message to make sure everything works properly. + +[1]: ../images/help/smssync/Screenshot_6_7_13_2_35_PM.jpg +[2]: ../images/help/smssync/initial.png +[3]: ../images/help/smssync/add_sync_url.png +[4]: ../images/help/smssync/Screenshot_6_7_13_3_11_PM-2.jpg +[5]: ../images/help/smssync/Screenshot_6_7_13_3_03_PM-2.jpg +[6]: ../images/help/smssync/SMSSync_Configure_your_Connections.jpg +[7]: ../images/help/smssync/Configure_your_Connections-2.jpg +[8]: ../images/help/smssync/Configure_your_Connections.jpg +[9]: ../images/help/smssync/Configure_your_Connections-5.jpg +[10]: ../images/help/smssync/add_sync_url.png +[11]: ../images/help/smssync/add_sync_url_filled.png +[12]: ../images/help/smssync/account_selected.png +[13]: ../images/help/smssync/photo_1-3.jpg +[14]: ../images/help/smssync/enable_sync_and_tasks.png +[15]: ../images/help/smssync/sync_frequency.png + diff --git a/plugins/frontlinesms-core/grails-app/conf/help/settings/smpp.txt b/plugins/frontlinesms-core/grails-app/conf/help/settings/smpp.txt new file mode 100644 index 000000000..3c0460bb0 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/conf/help/settings/smpp.txt @@ -0,0 +1,20 @@ +# Using SMPP with FrontlineSMS + +[The Short Message Peer-to-Peer (SMPP) protocol](http://en.wikipedia.org/wiki/Short_Message_Peer-to-Peer) is a protocol used to send messages over an internet connection to a Short Message Service Center (SMSC), which is typically owned by a mobile telephony provider. This is an advanced FrontlineSMS feature; most users who want to connect to Frontline to a mobile network should do so through [an Android phone][1], or [any other supported phone or usb modem][2]. + +The use of SMPP to send and receive messages requires a pre-negotiated agreement with the mobile provider, as the service is typically closed to the general public. In some cases, internet aggregators allow third party applications like FrontlineSMS to connect to them using the SMPP protocol, which is a more common use case for the FrontlineSMS SMPP functionality. + +The configuration details required to connect to an SMSC vary from case to case. The fields that are configurable in the SMPP connection in Frontline are: + +* the **URL** that the SMSC accepts SMPP traffic through +* the **port** that the SMSC expects traffic to be routed through +* the **username** for your account with this SMSC +* the **password** for your account +* the **From Number**, which will be the number that messages sent through this connection will originate from +* the name you want your connection to be saved under + +All fields except the name should be populated with values provided by the other party. + +[1]: ../settings/9.smssync +[2]: ../settings/4.setting_up_a_device + diff --git a/plugins/frontlinesms-core/web-app/help/core/status/1.getting_around_the_status_tab.txt b/plugins/frontlinesms-core/grails-app/conf/help/status/1.getting_around_the_status_tab.txt similarity index 79% rename from plugins/frontlinesms-core/web-app/help/core/status/1.getting_around_the_status_tab.txt rename to plugins/frontlinesms-core/grails-app/conf/help/status/1.getting_around_the_status_tab.txt index 918d13bf0..4b137bfdf 100644 --- a/plugins/frontlinesms-core/web-app/help/core/status/1.getting_around_the_status_tab.txt +++ b/plugins/frontlinesms-core/grails-app/conf/help/status/1.getting_around_the_status_tab.txt @@ -37,16 +37,16 @@ If you attach a device while FrontlineSMS is running, you may need to press the [Creating a new connection][5] [Deleting a Connection][4] -[1]: core/status/1.getting_around_the_status_tab -[2]: core/status/1.getting_around_the_status_tab -[3]: core/status/1.getting_around_the_status_tab -[4]: core/status/1.getting_around_the_status_tab -[5]: core/status/1.getting_around_the_status_tab -[6]: core/status/1.getting_around_the_status_tab -[7]: core/status/1.getting_around_the_status_tab -[8]: core/status/1.getting_around_the_status_tab -[9]: core/status/1.getting_around_the_status_tab -[10]: core/status/1.getting_around_the_status_tab -[11]: core/status/1.getting_around_the_status_tab -[12]: core/status/1.getting_around_the_status_tab +[1]: ../status/1.getting_around_the_status_tab +[2]: ../status/1.getting_around_the_status_tab +[3]: ../status/1.getting_around_the_status_tab +[4]: ../status/1.getting_around_the_status_tab +[5]: ../status/1.getting_around_the_status_tab +[6]: ../status/1.getting_around_the_status_tab +[7]: ../status/1.getting_around_the_status_tab +[8]: ../status/1.getting_around_the_status_tab +[9]: ../status/1.getting_around_the_status_tab +[10]: ../status/1.getting_around_the_status_tab +[11]: ../status/1.getting_around_the_status_tab +[12]: ../status/1.getting_around_the_status_tab [13]: ../images/help/status_overview.png diff --git a/plugins/frontlinesms-core/web-app/help/core/status/2.using_the_traffic_graph.txt b/plugins/frontlinesms-core/grails-app/conf/help/status/2.using_the_traffic_graph.txt similarity index 100% rename from plugins/frontlinesms-core/web-app/help/core/status/2.using_the_traffic_graph.txt rename to plugins/frontlinesms-core/grails-app/conf/help/status/2.using_the_traffic_graph.txt diff --git a/plugins/frontlinesms-core/grails-app/conf/spring/resources.groovy b/plugins/frontlinesms-core/grails-app/conf/spring/resources.groovy index fa950068b..11d3d143b 100644 --- a/plugins/frontlinesms-core/grails-app/conf/spring/resources.groovy +++ b/plugins/frontlinesms-core/grails-app/conf/spring/resources.groovy @@ -1,3 +1,7 @@ // Place your Spring DSL code here beans = { + multipartResolver(frontlinesms2.FrontlineMultipartResolver) { + maxInMemorySize = application.config.upload.maximum.size + maxUploadSize = application.config.upload.maximum.size + } } diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ActivityController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ActivityController.groovy index c71d1ce17..b4aedd745 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ActivityController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ActivityController.groovy @@ -2,7 +2,6 @@ package frontlinesms2 import grails.converters.JSON - class ActivityController extends ControllerUtils { static allowedMethods = [save: "POST", update: "POST", delete: "POST"] @@ -14,21 +13,45 @@ class ActivityController extends ControllerUtils { } def create() { - def groupList = Group.getGroupDetails() + SmartGroup.getGroupDetails() - [contactList: Contact.list(), - groupList:groupList] + [ + activityType: params.controller + ] } + def edit() { withActivity { activityInstance -> - def groupList = Group.getGroupDetails() + SmartGroup.getGroupDetails() def activityType = activityInstance.shortName - render view:"../$activityType/create", model:[contactList: Contact.list(), - groupList:groupList, - activityInstanceToEdit: activityInstance] + + def modelToRender = [ + activityInstanceToEdit: activityInstance, + activityType: activityType, + ] + + try { + //TODO These actually belong in the AutoforwardController + //but the method cannot, for some reason, be overriden + def groups = activityInstance.groups?:null + def smartGroups = activityInstance.smartGroups?:null + def contacts = activityInstance.contacts.findAll { it.name != '' }?:null + def addresses = activityInstance.contacts.findAll { it.name == '' }*.mobile?:null + + modelToRender.groups = groups + modelToRender.smartGroups = smartGroups + modelToRender.contacts = contacts + modelToRender.addresses = addresses + } catch (MissingPropertyException e) { + log.info "$e" + } + + render view:"../$activityType/create", model: modelToRender } } + def show() { + redirect controller:'message', action:'activity', params:params + } + def rename() {} def update() { @@ -110,25 +133,25 @@ class ActivityController extends ControllerUtils { def create_new_activity() {} - def getCollidingKeywords(topLevelKeywords) { + private def getCollidingKeywords(topLevelKeywords, instance) { if (topLevelKeywords == null) return [:] def collidingKeywords = [:] def currentKeyword topLevelKeywords.toUpperCase().split(",").collect { it.trim() }.each { currentKeyword = Keyword.getFirstLevelMatch(it) - if(currentKeyword) + if(currentKeyword && (currentKeyword.activity.id != instance.id)) collidingKeywords << [(currentKeyword.value):"'${currentKeyword.activity.name}'"] } - println "colliding keywords:: $collidingKeywords" + log.info "colliding keywords:: $collidingKeywords" return collidingKeywords } - protected void doSave(classShortname, service, instance) { + protected void doSave(service, instance, activate=true) { try { service.saveInstance(instance, params) - instance.activate() - flash.message = message(code:classShortname + '.saved') + if(activate) instance.activate() + flash.message = message([code:"${instance.class.shortName}.save.success", args:[instance.name]]) params.activityId = instance.id withFormat { json { render([ok:true, ownerId:instance.id] as JSON) } @@ -136,27 +159,29 @@ class ActivityController extends ControllerUtils { } } catch(Exception ex) { ex.printStackTrace() - def collidingKeywords = getCollidingKeywords(params.sorting == 'global'? '' : params.keywords) - def errors - if (collidingKeywords) { - errors = collidingKeywords.collect { - if(it.key == '') { - message(code:'activity.generic.global.keyword.in.use', args:[it.value]) - } else { - message(code:'activity.generic.keyword.in.use', args:[it.key, it.value]) - } - }.join('\n') - } else { - errors = instance.errors.allErrors.collect { - message(code:it.codes[0], args:it.arguments.flatten(), defaultMessage:it.defaultMessage) - }.join('\n') - } - withFormat { - json { render([ok:false, text:errors] as JSON) } - } + generateErrorMessages(instance) } } + private def generateErrorMessages(instance) { + def collidingKeywords = getCollidingKeywords(params.sorting == 'global'? '' : params.keywords, instance) + def errors + if (collidingKeywords) { + errors = collidingKeywords.collect { + if(it.key == '') { + message(code:'activity.generic.global.keyword.in.use', args:[it.value]) + } else { + message(code:'activity.generic.keyword.in.use', args:[it.key, it.value]) + } + }.join('\n') + } else { + errors = instance.errors.allErrors.collect { message(error:it) }.join('\n') + } + withFormat { + json { render([ok:false, text:errors] as JSON) } + } + } + private def withActivity = withDomainObject Activity private def defaultMessage(String code, args=[]) { diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/AnnouncementController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/AnnouncementController.groovy index 635224b62..411f1cd88 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/AnnouncementController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/AnnouncementController.groovy @@ -14,9 +14,7 @@ class AnnouncementController extends ActivityController { def save() { def announcementInstance = Announcement.get(params.ownerId)?: new Announcement() - doSave('announcement', announcementService, announcementInstance) + doSave(announcementService, announcementInstance) } - - private def withAnnouncement = withDomainObject Announcement } diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ApiController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ApiController.groovy index 2985bc8c1..8a95aed77 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ApiController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ApiController.groovy @@ -4,17 +4,16 @@ import frontlinesms2.api.* class ApiController extends ControllerUtils { def grailsApplication + def apiService + def index() { - println "entityClassApiUrl = $params?.entityClassApiUrl" - println "entityId = $params?.entityId" - println "params = $params" def entityClass = grailsApplication.domainClasses*.clazz.find { FrontlineApi.isAssignableFrom(it) && (it.getAnnotation(FrontlineApiAnnotations.class)?.apiUrl() == params.entityClassApiUrl) } def entity = entityClass?.findById(params.entityId) if(entity) { - entity.apiProcess(this) + apiService.invokeApiProcess(entity, this) } else { render text:'not found', status:404 } diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/AppInfoController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/AppInfoController.groovy new file mode 100644 index 000000000..e3886967e --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/AppInfoController.groovy @@ -0,0 +1,21 @@ +package frontlinesms2 + +import grails.converters.JSON + +class AppInfoController { + //FIXME index method should only accept GET requests but POST is set to prevent rendering of blank pages + static allowedMethods = [index: "POST"] + def appInfoService + + def index() { + render request.JSON.collectEntries { key, value -> + try { + [key, appInfoService.provide(this, key, value)] + } catch(Exception ex) { + log.warn("Problem processing app info key=$key, value=$value", ex) + [key, ex.message] + } + } as JSON + } +} + diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ArchiveController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ArchiveController.groovy index 9bec69865..80ab8a0cf 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ArchiveController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ArchiveController.groovy @@ -1,11 +1,6 @@ package frontlinesms2 class ArchiveController extends MessageController { -//> SERVICES - -//> INTERCEPTORS - -//> ACTIONS def index() { def action = params.messageSection ?: 'inbox' redirect action:action, params:params @@ -24,10 +19,5 @@ class ArchiveController extends MessageController { itemInstanceTotal: folderInstanceList.size(), messageSection: "folder"] } - -//> PRIVATE HELPERS - private def getShowModel(messageInstanceList) { - def model = super.getShowModel(messageInstanceList) - return model - } } + diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/AutoforwardController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/AutoforwardController.groovy index 309cfc4a6..9fb7b65f5 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/AutoforwardController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/AutoforwardController.groovy @@ -1,41 +1,21 @@ package frontlinesms2 -import grails.converters.JSON class AutoforwardController extends ActivityController { def autoforwardService def save() { - def autoforward - if(Autoforward.get(params.ownerId)) - autoforward = Autoforward.get(params.ownerId) - else - autoforward = new Autoforward() - try { - autoforwardService.saveInstance(autoforward, params) - flash.message = message(code:'autoforward.saved') - params.activityId = autoforward.id - withFormat { - json { render([ok:true, ownerId:autoforward.id] as JSON) } - html { [ownerId:autoforward.id] } - } - } catch (Exception e) { -// FIXME this looks like it was copy/pasted from ActivityController. Please refactor. - //first check if it is due to colliding keywords, so we can generate a more helpful message. - def collidingKeywords = getCollidingKeywords(params.sorting == 'global'? '' : params.keywords) - def errors - if (collidingKeywords) - errors = collidingKeywords.collect { - if(it.key == '') - message(code:'activity.generic.global.keyword.in.use', args: [it.value]) - else - message(code:'activity.generic.keyword.in.use', args: [it.key, it.value]) - }.join("\n") - else - errors = autoforward.errors.allErrors.collect {message(code:it.codes[0], args: it.arguments.flatten(), defaultMessage: it.defaultMessage)}.join("\n") - withFormat { - json { render([ok:false, text:errors] as JSON) } - } + withAutoforward { autoforward -> + doSave(autoforwardService, autoforward) } } + + def create() { + [ + messageText: '${message_text}', + activityType: params.controller + ] + } + + private def withAutoforward = withDomainObject Autoforward, { params.ownerId } } diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/AutoreplyController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/AutoreplyController.groovy index 55cb2a8e5..ee2c45e18 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/AutoreplyController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/AutoreplyController.groovy @@ -1,6 +1,5 @@ package frontlinesms2 -import grails.converters.JSON class AutoreplyController extends ActivityController { def autoreplyService @@ -8,12 +7,12 @@ class AutoreplyController extends ActivityController { // FIXME this should use withAutoreply to shorten and DRY the code, but it causes cascade errors as referenced here: // http://grails.1312388.n4.nabble.com/Cascade-problem-with-hasOne-relationship-td4495102.html def autoreply = Autoreply.get(params.ownerId)?: new Autoreply() - doSave('autoreply', autoreplyService, autoreply) + doSave(autoreplyService, autoreply) } def sendReply() { def autoreply = Autoreply.get(params.ownerId) - def incomingMessage = Fmessage.get(params.messageId) + def incomingMessage = TextMessage.get(params.messageId) params.addresses = incomingMessage.src params.messageText = autoreply.autoreplyText def outgoingMessage = messageSendService.createOutgoingMessage(params) diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ConnectionController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ConnectionController.groovy index e84705485..b51defee4 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ConnectionController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ConnectionController.groovy @@ -1,96 +1,143 @@ package frontlinesms2 import grails.converters.JSON - - class ConnectionController extends ControllerUtils { static allowedMethods = [save: "POST", update: "POST", delete:'GET'] + static final String RULE_PREFIX = "fconnection-" def fconnectionService def messageSendService + def smssyncService + def appSettingsService + def grailsApplication def index() { redirect action:'list' } - + def list() { def fconnectionInstanceList = Fconnection.list(params) def fconnectionInstanceTotal = Fconnection.count() + def appSettings = [:] + appSettings['routing.otherwise'] = appSettingsService.get("routing.otherwise") + appSettings['routing.use'] = appSettingsService.get("routing.use") + def fconnectionRoutingMap = getRoutingRules(appSettings['routing.use']) - def model = [connectionInstanceList:fconnectionInstanceList, - fconnectionInstanceTotal:fconnectionInstanceTotal] - if(!params.id) params.id = fconnectionInstanceList[0]?.id - if(params.id) model << show() - render view:'show', model:model - } - - def show() { + def model = [:] withFconnection { - if(params.createRoute) { - it.metaClass.getStatus = { ConnectionStatus.CONNECTING } - } - [connectionInstance: it] << [connectionInstanceList: Fconnection.list(params), - fconnectionInstanceTotal: Fconnection.list(params)] + model << [connectionInstance: it] + } + def newConnectionIds + if(flash.newConnectionIds) { + newConnectionIds = flash.newConnectionIds + flash.newConnectionIds = null } + [connectionInstanceList:fconnectionInstanceList, + fconnectionInstanceTotal:fconnectionInstanceTotal, + fconnectionRoutingMap:fconnectionRoutingMap, + newConnectionIds:newConnectionIds, + appSettings:appSettings] } def wizard() { if(params.id) { withFconnection { - return [action:'update', fconnectionInstance:it] + if(it.userMutable) { + return [action:'update', fconnectionInstance:it] + } } } else { return [action:'save'] } } - + def save() { remapFormParams() - doSave(Fconnection.implementations.find { it.shortName == params.connectionType }) + doSave(Fconnection.getImplementations(params).find { it.shortName == params.connectionType }) } def delete() { def connection = Fconnection.get(params.id) - if(connection.status == ConnectionStatus.NOT_CONNECTED) { + if(connection.status in [ConnectionStatus.DISABLED, ConnectionStatus.CONNECTING]) { connection.delete() flash.message = message code:'connection.deleted', args:[connection.name] - redirect action:'list' - } else throw new RuntimeException() + } else { + throw new RuntimeException() + } + redirect action:'list' } - + def update() { remapFormParams() withFconnection { fconnectionInstance -> fconnectionInstance.properties = params fconnectionInstance.validate() def connectionErrors = fconnectionInstance.errors.allErrors.collect { message(error:it) } - if (fconnectionInstance.save()) { - withFormat { - html { - flash.message = LogEntry.log(message(code: 'default.created.message', args: [message(code: 'fconnection.name'), fconnectionInstance.id])) - redirect(controller:'connection', action: "createRoute", id: fconnectionInstance.id) + if(fconnectionInstance.save()) { + withFormat { + html { + flash.message = LogEntry.log(message(code: 'default.created.message', args: [message(code: 'fconnection.name'), fconnectionInstance.id])) + redirect(controller:'connection', action: 'enable', id: fconnectionInstance.id) + } + json { + render([ok:true, redirectUrl:createLink(action:'enable', id:fconnectionInstance.id)] as JSON) + } } - json { - render([ok:true, redirectUrl:createLink(action:'createRoute', id:fconnectionInstance.id)] as JSON) + } else { + withFormat { + html { + flash.message = LogEntry.log(message(code: 'connection.creation.failed', args:[fconnectionInstance.errors])) + redirect(controller:'connection', action:"list") + } + json { + render([ok:false, text:connectionErrors.join().toString()] as JSON) + } } } - } else { - withFormat { - html { - flash.message = LogEntry.log(message(code: 'connection.creation.failed', args:[fconnectionInstance.errors])) - redirect(controller:'connection', action:"list") - } - json { - render([ok:false, text:connectionErrors.join().toString()] as JSON) + } + } + + def changeRoutingPreferences() { + appSettingsService.set('routing.use', params.routingUseOrder) + redirect action:'list' + } + + private getRoutingRules(routingRules) { + def fconnectionRoutingList = [] + def fconnectionRoutingMap = [:] + def connectionInstanceList = Fconnection.findAllBySendEnabled(true) + + if(routingRules) { + fconnectionRoutingList = routingRules.split(/\s*,\s*/) + + // Replacing fconnection rules with fconnection instances + fconnectionRoutingList = fconnectionRoutingList.collect { rule -> + if(rule.startsWith(RULE_PREFIX)) { + connectionInstanceList.find { + it.id == ((rule - RULE_PREFIX) as Integer) + } + } else rule + } + + if(fconnectionRoutingList) { + def length = fconnectionRoutingList.size() + if(!fconnectionRoutingList.contains("uselastreceiver")) fconnectionRoutingList << "uselastreceiver" + ((fconnectionRoutingList += connectionInstanceList) - null as Set).eachWithIndex { it, index -> + fconnectionRoutingMap[it] = index < length } } + + } else { + fconnectionRoutingList << "uselastreceiver" + ((fconnectionRoutingList + connectionInstanceList) as Set).findAll{ fconnectionRoutingMap[it] = false } } - } + + fconnectionRoutingMap } - + private def remapFormParams() { def cType = params.connectionType - if(!(cType in Fconnection.implementations*.shortName)) { + if(!(cType in Fconnection.getImplementations(params)*.shortName)) { throw new RuntimeException("Unknown connection type: " + cType) } def newParams = [:] // TODO remove this - without currently throw ConcurrentModificationException @@ -106,32 +153,39 @@ class ConnectionController extends ControllerUtils { } params << newParams } - - def createRoute() { - CreateRouteJob.triggerNow([connectionId:params.id]) - params.createRoute = true - flash.message = message(code: 'connection.route.connecting') - redirect(action:'list', params:params) + + def enable() { + if (Fconnection.get(params.id)?.userMutable) { + EnableFconnectionJob.triggerNow([connectionId:params.id]) + sleep 100 // This horrible hack allows enough time for the job to start before we try to get the status of the connection we're enabling + def connectionInstance = Fconnection.get(params.id) + if(connectionInstance?.shortName == 'smssync') { // FIXME should not be connection-specific code here + smssyncService.startTimeoutCounter(connectionInstance) + } + } + flash.newConnectionIds = params.id + redirect(action:'list', params: params) } - - def destroyRoute() { + + def disable() { withFconnection { c -> - fconnectionService.destroyRoutes(c) - flash.message = message(code: 'connection.route.disconnecting') + if(c.userMutable) { + fconnectionService.disableFconnection(c) + } redirect(action:'list', id:c.id) } } def listComPorts() { // This is a secret debug method for now to help devs see what ports are available - render(text: "${serial.CommPortIdentifier.portIdentifiers*.name}") + render text:serial.CommPortIdentifier.portIdentifiers*.name } def createTest() { def connectionInstance = Fconnection.get(params.id) [connectionInstance:connectionInstance] } - + def sendTest() { withFconnection { connection -> def m = messageSendService.createOutgoingMessage(params) @@ -140,20 +194,74 @@ class ConnectionController extends ControllerUtils { redirect action:'list', id:params.id } } - + + private doAfterSaveOperations(fconnectionInstance) { + def serviceName = "${fconnectionInstance.shortName}Service" + def service = grailsApplication.mainContext[serviceName] + if(service) { + def methodName = 'afterSave' + if(service.respondsTo(methodName) as boolean) { + service."$methodName"(fconnectionInstance) + } + } + } + private def doSave(Class clazz) { - def fconnectionInstance = clazz.newInstance() - fconnectionInstance.properties = params - fconnectionInstance.validate() - def connectionErrors = fconnectionInstance.errors.allErrors.collect { message(error:it) } - if (fconnectionInstance.save()) { + def connectionService = grailsApplication.mainContext["${clazz.shortName}Service"] + def saveSuccessful + def connectionErrors + def fconnectionInstance + if (connectionService && connectionService.respondsTo('handleSave')) { + def handleSaveResponse = connectionService.handleSave(params) + saveSuccessful = handleSaveResponse.success + connectionErrors = handleSaveResponse.errors + def fconnectionInstances = handleSaveResponse.connectionInstance + flash.newConnectionIds = (fconnectionInstances instanceof List ? fconnectionInstances*.id.join(',') : fconnectionInstances?.id) + withFormat { + html { + flash.message = LogEntry.log(saveSuccessful ? handleSaveResponse.successMessage : message(code: 'connection.creation.failed', args:[handleSaveResponse.errors])) + redirect(controller:'connection', action:"list") // FIXME - should just enable connection here and redirect to list action, surely! + } + json { + + render((saveSuccessful ? [ok:true, redirectUrl:createLink(action:'list')] : [ok:false, text:handleSaveResponse.errors.join(", ")]) as JSON) + } + } + return + } + else { + fconnectionInstance = clazz.newInstance() + fconnectionInstance.properties = params + fconnectionInstance.validate() + connectionErrors = fconnectionInstance.errors.allErrors.collect { message(error:it) } + saveSuccessful = fconnectionInstance.save(flush:true) + } + if(saveSuccessful) { + doAfterSaveOperations(fconnectionInstance) + def connectionUseSetting = appSettingsService['routing.use'] + def retainedRules = [] + if (connectionUseSetting) { + connectionUseSetting.split(',').each { rule -> + if(rule.startsWith('fconnection-')) { + if(Fconnection.countById(rule.split('-')[1] as int)) { + retainedRules << rule + } + } + else { + retainedRules << rule + } + } + } + appSettingsService['routing.use'] = retainedRules? + "${retainedRules.join(',')},fconnection-$fconnectionInstance.id": + "fconnection-$fconnectionInstance.id" withFormat { html { flash.message = LogEntry.log(message(code: 'default.created.message', args: [message(code: 'fconnection.name', default: 'Fconnection'), fconnectionInstance.id])) - forward(action:"createRoute", id:fconnectionInstance.id) + forward action:'enable', id:fconnectionInstance.id } json { - render([ok:true, redirectUrl:createLink(action:'createRoute', id:fconnectionInstance.id)] as JSON) + render([ok:true, redirectUrl:createLink(action:'enable', id:fconnectionInstance.id)] as JSON) } } } else { @@ -163,12 +271,20 @@ class ConnectionController extends ControllerUtils { redirect(controller:'connection', action:"list") } json { - render([ok:false, text:connectionErrors.join(", ").toString()] as JSON) + render([ok:false, text:connectionErrors.unique().join(", ").toString()] as JSON) } } } } + def routingRules() { + def appSettings = [:] + appSettings['routing.otherwise'] = appSettingsService.get("routing.otherwise") + appSettings['routing.use'] = appSettingsService.get("routing.use") + def fconnectionRoutingMap = getRoutingRules(appSettings['routing.use']) + render(template:'routing', model:[appSettings: appSettings, fconnectionRoutingMap: fconnectionRoutingMap]) + } + private def withFconnection = withDomainObject Fconnection, { params.id }, { redirect(controller:'connection', action:'list') } } diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ContactController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ContactController.groovy index 0b702025b..0c9649689 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ContactController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ContactController.groovy @@ -10,10 +10,18 @@ class ContactController extends ControllerUtils { //> SERVICES def grailsApplication def contactSearchService + def appSettingsService //> INTERCEPTORS def beforeInterceptor = { - params.max = params.max?: grailsApplication.config.grails.views.pagination.max + def maxConfigured = grailsApplication.config.grails.views.pagination.max + + def maxRequested = { + try { + return params.max as Integer + } catch(Exception _) {} + }.call()?: Integer.MAX_VALUE + params.max = Math.min(maxRequested, maxConfigured) params.sort = params.sort ?: 'name' params.offset = params.offset ?: 0 true @@ -21,7 +29,7 @@ class ContactController extends ControllerUtils { //> ACTIONS def index() { - redirect action: "show", params:params + redirect action:'show', params:params } def getUniqueCustomFields() { @@ -32,7 +40,17 @@ class ContactController extends ControllerUtils { } } } - + + def disableWarning() { + def warning = params.warning + if(warning == "NonNumericNotAllowedWarning") { + appSettingsService.set("non.numeric.characters.removed.warning.disabled", true) + } else if(warning =="l10nWarning") { + appSettingsService.set("international.number.format.warning.disabled", true) + } + render ([ok:true] as JSON) + } + def updateContactPane() { def contactInstance = Contact.get(params.id) def usedFields = contactInstance?.customFields ?: [] @@ -54,18 +72,14 @@ class ContactController extends ControllerUtils { uniqueFieldInstanceList: unusedFields, fieldInstanceList: CustomField.findAll(), groupInstanceList: Group.findAll(), - groupInstanceTotal: Group.count(), smartGroupInstanceList: SmartGroup.list()] - render view:'/contact/_single_contact_view', model:model + render view:'/contact/_single_contact', model:model } - + def show() { - if(params.flashMessage) { - flash.message = params.flashMessage - } def contactList = contactSearchService.contactList(params) def contactInstanceList = contactList.contactInstanceList - def contactInstanceTotal = contactList.contactInstanceTotal + def contactInstanceTotal = Contact.count() def contactInstance = (params.contactId ? Contact.get(params.contactId) : (contactInstanceList ? contactInstanceList[0] : null)) def usedFields = contactInstance?.customFields ?: [] def usedFieldNames = [] @@ -79,11 +93,26 @@ class ContactController extends ControllerUtils { unusedFields.add(it) } def contactGroupInstanceList = contactInstance?.groups ?: [] + if(params.contactId && !contactInstance) { + flash.message = message(code:'contact.not.found') + redirect action:'show' + return false + } else if(params.groupId && !contactList.contactsSection) { + flash.message = message(code:'group.not.found') + redirect action:'show' + return false + } else if(params.smartGroupId && !contactList.contactsSection) { + flash.message = message(code:'smartgroup.not.found') + redirect action:'show' + return false + } + [contactInstance: contactInstance, checkedContactList: ',', contactInstanceList: contactInstanceList, contactInstanceTotal: contactInstanceTotal, contactsSection: contactList.contactsSection, + contactsSectionContactTotal: contactList.contactsSectionContactTotal, contactFieldInstanceList: usedFields, contactGroupInstanceList: contactGroupInstanceList, contactGroupInstanceTotal: contactGroupInstanceList.size(), @@ -91,10 +120,9 @@ class ContactController extends ControllerUtils { uniqueFieldInstanceList: unusedFields, fieldInstanceList: CustomField.findAll(), groupInstanceList: Group.findAll(), - groupInstanceTotal: Group.count(), smartGroupInstanceList: SmartGroup.list()] } - + def createContact() { render view:'show', model: [contactInstance: new Contact(params), contactFieldInstanceList: [], @@ -104,48 +132,79 @@ class ContactController extends ControllerUtils { uniqueFieldInstanceList: CustomField.getAllUniquelyNamed(), fieldInstanceList: CustomField.findAll(), groupInstanceList: Group.findAll(), - groupInstanceTotal: Group.count(), smartGroupInstanceList: SmartGroup.list()] << contactSearchService.contactList(params) } def saveContact() { def contactInstance = Contact.get(params.contactId) ?: new Contact() contactInstance.properties = params + def saveSuccessful = false if(attemptSave(contactInstance)) { parseContactFields(contactInstance) - attemptSave(contactInstance) + saveSuccessful = attemptSave(contactInstance) + } + if(request.xhr) { + def data = [success:saveSuccessful, + flagCSSClasses: contactInstance.flagCSSClasses, + contactPrettyPhoneNumber: contactInstance.mobile?.toPrettyPhoneNumber() ] << getContactErrors(contactInstance) + render (data as JSON) + } else { + redirect action:'show', params:[contactId:contactInstance.id] } - redirect(action:'show', params:[contactId:contactInstance.id]) } - + + private getContactErrors(contactInstance) { + contactInstance.validate() + def data = [errors:[:]] + contactInstance.errors.allErrors.each { + def field = it.field + def errorMessage = g.message(error:it) + if(data.errors."$field") { + data.errors."$field" << errorMessage + } else { + data.errors."$field" = [errorMessage] + } + } + log.info "##### ${data}" + return data + } + def update() { withContact { contactInstance -> contactInstance.properties = params parseContactFields(contactInstance) attemptSave(contactInstance) - if(params.groupId) redirect(controller: params.contactsSection, action: 'show', id: params.groupId, params:[contactId: contactInstance.id, sort:params.sort, offset: params.offset]) - else redirect(action:'show', params:[contactId: contactInstance.id, offset:params.offset], max:params.max) + if(params.groupId) { + redirect(action:'show', controller:params.contactsSection, id: params.groupId, params:[contactId: contactInstance.id, sort:params.sort, offset:params.offset]) + } else { + redirect(action:'show', params:[contactId:contactInstance.id, offset:params.offset], max:params.max) + } } } - + def updateMultipleContacts() { + params.remove("mobile") //TODO remove on refactor of contact form getCheckedContacts().each { c -> parseContactFields(c) attemptSave(c) } - render(view:'show', model: show()) + flash.message = message(code:'default.updated.multiple', args:[message(code:'contact.label')]) + render view:'show', model:show() } - + def confirmDelete() { def contactInstanceList = getCheckedContacts() [contactInstanceList:contactInstanceList, contactInstanceTotal:contactInstanceList.size()] } - + def delete() { - getCheckedContacts()*.delete() + // FIXME looks like someone doesn't know what's going wrong here and clutching at straws + Contact.withTransaction { status -> + getCheckedContacts()*.delete() + } flash.message = message(code:'default.deleted', args:[message(code:'contact.label')]) - redirect(action: "show") + redirect action:'show' } def newCustomField() { @@ -157,47 +216,41 @@ class ContactController extends ControllerUtils { } def search() { - render template:'search_results', model:contactSearchService.contactList(params) + render(template:'search_results', model:contactSearchService.contactList(params)) } - + def checkForDuplicates() { def foundContact = Contact.findByMobile(params.mobile) if (foundContact && foundContact.id.toString() == params.contactId) { render true - } else - if(!foundContact && params.mobile) render true - else render false - } - - def messageStats() { - def contactInstance = Contact.get(params.id) - if(contactInstance) { - def messageStats = [inboundMessagesCount: contactInstance.inboundMessagesCount, outboundMessagesCount: contactInstance.outboundMessagesCount] - render messageStats as JSON + } else { + render (!foundContact && params.mobile) } } //> PRIVATE HELPER METHODS private def attemptSave(contactInstance) { - def existingContact = params.mobile ? Contact.findByMobileLike(params.mobile) : null - if (contactInstance.save()) { - flash.message = message(code:'default.updated', args:[message(code:'contact.label'), contactInstance.name]) + def mobile = params.mobile?.replaceAll(/\D/, '') + if(params.mobile && params.mobile[0] == '+') mobile = '+' + mobile + def existingContact = mobile ? Contact.findByMobileLike(mobile) : null + if (existingContact && existingContact != contactInstance) { + flash.message = "${message(code: 'contact.exists.warn')} " + g.link(action:'show', params:[contactId:Contact.findByMobileLike(params.mobile)?.id], g.message(code: 'contact.view.duplicate')) + return false + } + if(contactInstance.save()) { def redirectParams = [contactId: contactInstance.id] if(params.groupId) redirectParams << [groupId: params.groupId] return true - } else if (existingContact && existingContact != contactInstance) { - flash.message = "${message(code: 'contact.exists.warn')} " + g.link(action:'show', params:[contactId:Contact.findByMobileLike(params.mobile)?.id], g.message(code: 'contact.view.duplicate')) - return false } return false } def multipleContactGroupList() { def groups = Group.getGroupLists(getCheckedContactIds()) - render(view: "_multiple_contact_view", model: [sharedGroupInstanceList:groups.shared, + render(view: '_multiple_contact', model: [sharedGroupInstanceList:groups.shared, nonSharedGroupInstanceList:groups.nonShared]) } - + private def withContact = withDomainObject Contact, { params.contactId } private def getCheckedContacts() { @@ -206,7 +259,7 @@ class ContactController extends ControllerUtils { private def getCheckedContactIds() { def ids = params['contact-select']?: - params.checkedContactList? params.checkedContactList.tokenize(',').unique(): + params.checkedContactList? params.checkedContactList?.tokenize(',')?.unique(): [params.contactId] return ids.flatten().unique() } @@ -216,17 +269,17 @@ class ContactController extends ControllerUtils { updateGroups(contactInstance) return contactInstance.stripNumberFields() } - + private def updateGroups(Contact contactInstance) { - def groupsToAdd = params.groupsToAdd.tokenize(',').unique() - def groupsToRemove = params.groupsToRemove.tokenize(',') - + def groupsToAdd = params.groupsToAdd?.tokenize(',')?.unique() + def groupsToRemove = params.groupsToRemove?.tokenize(',') + // Check for errors in groupsToAdd and groupsToRemove - if(!groupsToAdd.disjoint(groupsToRemove)) { + if(!groupsToAdd?.disjoint(groupsToRemove)) { contactInstance.errors.reject(message(code: 'contact.addtogroup.error')) return false } - + groupsToRemove.each() { id -> contactInstance.removeFromGroups(Group.get(id)) } @@ -235,42 +288,36 @@ class ContactController extends ControllerUtils { } return contactInstance } - + private def updateCustomFields(Contact contactInstance) { - def fieldsToAdd = params.fieldsToAdd ? params.fieldsToAdd.tokenize(',') : [] - def fieldsToRemove = params.fieldsToRemove ? params.fieldsToRemove.tokenize(',') : [] - - fieldsToAdd.each() { name -> - def existingFields = CustomField.findAllByNameAndContact(name, contactInstance) - def fieldsByName = params."$name" - if(fieldsByName?.class != String) { - fieldsByName.each() { val -> - if(val != "" && !existingFields.value.contains(val)) - contactInstance.addToCustomFields(new CustomField(name: name, value: val)).save(flush:true) - existingFields = CustomField.findAllByNameAndContact(name, contactInstance) - } - } else if(fieldsByName != "" && !existingFields.value.contains(fieldsByName)) { - contactInstance.addToCustomFields(new CustomField(name: name, value: fieldsByName)) + def fieldsToAdd = params.fieldsToAdd?.tokenize(',') + def fieldsToRemove = params.fieldsToRemove?.tokenize(',') + def existingFields = CustomField.findAllByContact(contactInstance) + + fieldsToAdd?.each { name -> + def fieldsByName = params["newCustomField-$name"] + if(fieldsByName && !(name in existingFields*.name)) { + contactInstance.addToCustomFields(new CustomField(name:name, value:fieldsByName)) } } - fieldsToRemove.each() { id -> - def toRemove = CustomField.get(id) - contactInstance.removeFromCustomFields(toRemove) - if(toRemove) + + fieldsToRemove?.each { name -> + def toRemove = CustomField.findByContactAndName(contactInstance, name) + if(toRemove) { + contactInstance.removeFromCustomFields(toRemove) toRemove.delete() + } } //also save any existing fields that have changed - def existingFields = CustomField.findAllByContact(contactInstance) - existingFields.each() { existingField -> - def newValue = params."$existingField.name" - if (newValue && (existingField.value != newValue)) - { - existingField.value = newValue - existingField.save() + existingFields.each { f -> + def newValue = params["customField-$f.id"] + if(newValue && f.value != newValue) { + f.value = newValue + f.save() } } - return contactInstance } } + diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/CustomactivityController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/CustomactivityController.groovy new file mode 100644 index 000000000..f261fb9ab --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/CustomactivityController.groovy @@ -0,0 +1,13 @@ +package frontlinesms2 + +class CustomactivityController extends ActivityController { + def customActivityService + + def save() { + withCustomActivity { customActivity -> + doSave(customActivityService, customActivity) + } + } + + private def withCustomActivity = withDomainObject CustomActivity, { params.ownerId } +} diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ErrorController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ErrorController.groovy index e1367f12c..2ec1c06ab 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ErrorController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ErrorController.groovy @@ -4,7 +4,7 @@ import java.text.DateFormat import java.text.SimpleDateFormat class ErrorController extends ControllerUtils { - static final def DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd") + private final def DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd") def logs() { supplyDownload('log') } diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ExportController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ExportController.groovy index f5970865e..819ee8b78 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ExportController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ExportController.groovy @@ -1,7 +1,7 @@ package frontlinesms2 -import java.text.DateFormat; import java.text.SimpleDateFormat +import ezvcard.* class ExportController extends ControllerUtils { def exportService @@ -29,39 +29,39 @@ class ExportController extends ControllerUtils { def downloadMessageReport() { def messageSection = params.messageSection - def messageInstanceList + def interactionInstanceList //TODO Clean up switch mess switch(messageSection) { case 'inbox': - messageInstanceList = Fmessage.inbox(params.starred, params.viewingArchive).list() + interactionInstanceList = TextMessage.inbox(params.starred, params.viewingArchive).list() break case 'sent': - messageInstanceList = Fmessage.sent(params.starred, params.viewingArchive).list() + interactionInstanceList = TextMessage.sent(params.starred, params.viewingArchive).list() break case 'pending': - messageInstanceList = Fmessage.listPending(params.failed?:false, [:]) + interactionInstanceList = TextMessage.listPending(params.failed, params) break case 'trash': - messageInstanceList = Fmessage.trash().list() + interactionInstanceList = TextMessage.trash().list() break case 'activity': - messageInstanceList = Activity.get(params.ownerId).getActivityMessages(params.starred?:false, params.inbound).list() + interactionInstanceList = Activity.get(params.ownerId).getActivityMessages(params.starred?:false, params.inbound) break case 'folder': - messageInstanceList = Folder.get(params.ownerId).getFolderMessages(params.starred?:false, params.inbound).list() + interactionInstanceList = Folder.get(params.ownerId).getFolderMessages(params.starred?:false, params.inbound).list() break case 'radioShow': - messageInstanceList = MessageOwner.get(params.ownerId).getShowMessages().list() + interactionInstanceList = MessageOwner.get(params.ownerId).getShowMessages().list() break case 'result': - messageInstanceList = Fmessage.search(Search.get(params.searchId)).list() + interactionInstanceList = TextMessage.search(Search.get(params.searchId)).list() break default: - messageInstanceList = Fmessage.findAll() + interactionInstanceList = TextMessage.findAll() break } - generateMessageReport(messageInstanceList.unique()) + generateMessageReport(interactionInstanceList.unique()) } def contactWizard() { @@ -81,30 +81,53 @@ class ExportController extends ControllerUtils { } else { throw new RuntimeException("Unrecognised section: $params.contactsSection") } - generateContactReport(contactInstanceList) + if(params.format == 'vcf') { + exportContactVcf(contactInstanceList) + } else { + generateContactReport(contactInstanceList) + } + } + + private def exportContactVcf(contactInstanceList) { + response.setHeader 'Content-disposition', + "attachment; filename=FrontlineSMS_Contact_Export_${formatedTime}.${params.format}" + response.setHeader 'Content-Type', 'text/vcard' + def cards = contactInstanceList.collect { c -> + def v = new VCard() + v.setFormattedName(c.name) + v.addTelephoneNumber(c.mobile) + v.addEmail(c.email) + return v + } + render text:Ezvcard.write(cards).go() } - private def generateMessageReport(messageInstanceList) { - def currentTime = new Date() - def formatedTime = dateToString(currentTime) - List fields = ["id", "inboundContactName", "src", "outboundContactList", "dispatches.dst", "text", "date"] - Map labels = ["id":message(code: 'export.database.id'), "inboundContactName":message(code: 'export.message.source.name'),"src":message(code: 'export.message.source.mobile'), "outboundContactList":message(code: 'export.message.destination.name'), "dispatches.dst":message(code: 'export.message.destination.mobile'), "text":message(code: 'export.message.text'), "date":message(code: 'export.message.date.created')] + private def generateMessageReport(interactionInstanceList) { + List fields = ["id", "inboundContactName", "src", "outboundContactList", "dst", "text", "date"] + def thisInteractionAsMap + def interactions = interactionInstanceList.collect { interaction -> + thisInteractionAsMap = [:] + fields.each { field -> + thisInteractionAsMap."$field" = (field == 'dst') ? interaction.dispatches*.dst : interaction."$field" + } + thisInteractionAsMap + } + Map labels = ["id":message(code: 'export.database.id'), "inboundContactName":message(code: 'export.message.source.name'),"src":message(code: 'export.message.source.mobile'), "outboundContactList":message(code: 'export.message.destination.name'), "dst":message(code: 'export.message.destination.mobile'), "text":message(code: 'export.message.text'), "date":message(code: 'export.message.date.created')] Map parameters = [title: message(code: 'export.message.title')] + setUnicodeParameter(parameters) response.setHeader("Content-disposition", "attachment; filename=FrontlineSMS_Message_Export_${formatedTime}.${params.format}") - try{ - exportService.export(params.format, response.outputStream, messageInstanceList, fields, labels, [:], parameters) - } catch(Exception e){ + try { + exportService.export(params.format, response.outputStream, interactions, fields, labels, [:], parameters) + } catch(Exception e) { render(text: message(code: 'report.creation.error')) } - [messageInstanceList: messageInstanceList] + [interactionInstanceList: interactionInstanceList] } private def generateContactReport(contactInstanceList) { - def currentTime = new Date() - def formatedTime = dateToString(currentTime) List fields = ["name", "mobile", "email", "notes", "groupMembership"] Map labels = params.format == "csv" ? - ["name":"Name", "mobile":"Mobile Number", "email":"E-mail Address", "notes":"Notes", "groupMembership":"Group(s)"] + ["name":"Name", "mobile":"Mobile Number", "email":"Email", "notes":"Notes", "groupMembership":"Group(s)"] : ["name":message(code: 'export.contact.name'), "mobile":message(code: 'export.contact.mobile'), "email":message(code: 'export.contact.email'), "notes":message(code: 'export.contact.notes'), "groupMembership":message(code: 'export.contact.groups')] // add custom fields def customFields = CustomField.getAllUniquelyNamed() @@ -120,15 +143,23 @@ class ExportController extends ControllerUtils { contact.metaClass.groupMembership = contact.groups*.name.join("\\\\") } Map parameters = [title: message(code: 'export.contact.title')] + setUnicodeParameter(parameters) response.setHeader("Content-disposition", "attachment; filename=FrontlineSMS_Contact_Export_${formatedTime}.${params.format}") - try{ + try { exportService.export(params.format, response.outputStream, contactInstanceList, fields, labels, [:],parameters) - } catch(Exception e){ + } catch(Exception e) { render(text: message(code: 'report.creation.error')) } [contactInstanceList: contactInstanceList] } + private setUnicodeParameter(parameters) { + if(params.format == 'pdf') { + parameters << ["pdf.encoding":"UniGB-UCS2-H", "font.family": "STSong-Light"] + } else if(params.format == "csv"){ + parameters << ['encoding':'UTF-8'] + } + } private def getActivityDescription() { if(params.ownerId){ def messageOwner = MessageOwner.findById(params.ownerId) @@ -153,12 +184,8 @@ class ExportController extends ControllerUtils { } } - private String dateToString(Date date) { - DateFormat formatedDate = createDateFormat() - return formatedDate.format(date) - } - - private DateFormat createDateFormat() { - return new SimpleDateFormat("yyyyMMdd", request.locale) + private String getFormatedTime() { + new SimpleDateFormat("yyyyMMdd", request.locale).format(new Date()) } } + diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/FrontlinesyncController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/FrontlinesyncController.groovy new file mode 100644 index 000000000..3d7f5ae5c --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/FrontlinesyncController.groovy @@ -0,0 +1,18 @@ +package frontlinesms2 + +import grails.converters.JSON + +class FrontlinesyncController extends ControllerUtils { + def frontlinesyncService + def index() { redirect action:'update' } + + def update() { + withFconnection { frontlinesyncInstance -> + frontlinesyncService.updateSyncConfig(params, frontlinesyncInstance, false) + render ([success:true] as JSON) + } + } + + private def withFconnection = withDomainObject Fconnection, { params.id }, { redirect(controller:'connection', action:'list') } +} + diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/GroupController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/GroupController.groovy index db01412c7..08edff273 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/GroupController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/GroupController.groovy @@ -5,6 +5,8 @@ import grails.converters.JSON class GroupController extends ControllerUtils { static allowedMethods = [update: "POST"] + def groupService + def list() { [groups:Group.list()] } @@ -45,7 +47,7 @@ class GroupController extends ControllerUtils { flash.message = message(code:'default.created.message', args:[message(code:'group.label'), groupInstance.name]) withFormat { json { - render([ok:true] as JSON) + render([ok:true, id:groupInstance.id, name:groupInstance.name] as JSON) } } } else { @@ -64,11 +66,9 @@ class GroupController extends ControllerUtils { } def delete() { - try { - Group.get(params.id)?.delete(flush: true) + if(groupService.delete(Group.get(params.id))) { flash.message = message(code:'default.deleted.message', args:[message(code:'group.label')]) - } - catch (org.springframework.dao.DataIntegrityViolationException e) { + } else { flash.message = message(code:'group.delete.fail') } redirect controller:'contact', action: "show" diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/HelpController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/HelpController.groovy index f1dd642ea..074935073 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/HelpController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/HelpController.groovy @@ -1,36 +1,35 @@ package frontlinesms2 -import frontlinesms2.* import grails.converters.JSON class HelpController extends ControllerUtils { def appSettingsService - def index() { redirect action:'main' } - - def main() {} + def index() {} def section() { - def helpText - if(params.helpSection) { - // FIXME this is open to injection attacks - def markdownFile = new File("web-app/help/" + params.helpSection + ".txt") - if (markdownFile.canRead()) { - helpText = markdownFile.text - } - } - if(!helpText) helpText = "This help file is not yet available, sorry." + def helpText = this.class.getResource("/help/${params.helpSection}.txt")?.text + if(!helpText) helpText = g.message(code:"help.notfound") render text:helpText.markdownToHtml() } + + def image() { + def uri = r.resource(uri:"/images/help/${params.imagePath}.png") + log.info "HelpController.image()::::$uri" + uri = uri.substring(request.contextPath.size()) + redirect(uri:uri, absolute:true) + } + def updateShowNewFeatures() { - appSettingsService['newfeatures.popup.show.immediately'] = false appSettingsService['newfeatures.popup.show.infuture'] = params.enableNewFeaturesPopup?: false appSettingsService.persist() render text:[] as JSON } def newfeatures() { - params.helpSection = 'core/features/new' + appSettingsService['newfeatures.popup.show.immediately'] = false + appSettingsService.persist() + params.helpSection = 'frontlinesms-core/features/new' section() } } diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ImportController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ImportController.groovy index 565e8652b..4aca0d7ff 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ImportController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/ImportController.groovy @@ -1,197 +1,174 @@ -package frontlinesms2 - -import java.text.DateFormat; -import java.text.SimpleDateFormat - -import au.com.bytecode.opencsv.CSVWriter - -class ImportController extends ControllerUtils { - private static final def MESSAGE_DATE = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") - - def importData() { - if (params.data == 'contacts') importContacts() - else importMessages() - } - - def importContacts() { - def savedCount = 0 - def uploadedCSVFile = request.getFile('importCsvFile') - - if(uploadedCSVFile) { - def headers - def failedLines = [] - def standardFields = ['Name':'name', 'Mobile Number':'mobile', - 'E-mail Address':'email', 'Notes':'notes'] - uploadedCSVFile.inputStream.toCsvReader([escapeChar:'�']).eachLine { tokens -> - if(!headers) headers = tokens - else try { - Contact c = new Contact() - def groups - def customFields = [] - headers.eachWithIndex { key, i -> - def value = tokens[i] - if(key in standardFields) { - c."${standardFields[key]}" = value - } else if(key == 'Group(s)') { - def groupNames = getGroupNames(value) - groups = getGroups(groupNames) - } else { - if (value.size() > 0 ){ - customFields << new CustomField(name:key, value:value) - } - } - } - // TODO not sure why this has to be done in a new session, but grails - // can't cope with failed saves if we don't do this - Contact.withNewSession { - c.save(failOnError:true) - if(groups) groups.each { c.addToGroup(it) } - if(customFields) customFields.each { c.addToCustomFields(it) } - c.save() - } - ++savedCount - } catch(Exception ex) { - log.info message(code: 'import.contact.save.error'), ex - failedLines << tokens - } - } - - if(failedLines) { - def writer - try { - writer = new CSVWriter(new OutputStreamWriter(failedContactsFile.newOutputStream(), 'UTF-8')) - writer.writeNext(headers) - failedLines.each { writer.writeNext(it) } - } finally { try { writer.close() } catch(Exception ex) {} } - } - - flash.message = g.message(code:'import.contact.complete', - args:[savedCount, failedLines.size()]) - if(failedLines) flash.message += '\n' + g.link(action:'failedContacts', - params:[jobId:params.jobId], - g.message(code:'import.contact.failed.download')) - - redirect controller:'settings', action:'general' - } else throw new RuntimeException(message(code:'import.upload.failed')) - } - - def failedContacts() { - response.setHeader("Content-disposition", "attachment; filename=failedContacts.csv") - failedContactsFile.eachLine { response.outputStream << it << '\n' } - response.outputStream.flush() - } - - def importMessages() { - def savedCount = 0 - def failedCount = 0 - def importingVersionOne = true - def uploadedCSVFile = request.getFile('importCsvFile') - if(uploadedCSVFile) { - def headers - def standardFields = ['Message Content':'text', 'Sender Number':'src'] - def dispatchStatuses = [Failed:DispatchStatus.FAILED, - Pending:DispatchStatus.PENDING, - Outbox:DispatchStatus.SENT, - Sent:DispatchStatus.SENT] - uploadedCSVFile.inputStream.toCsvReader([escapeChar:'�']).eachLine { tokens -> - println "Processing: $tokens" - if(!headers) { - headers = tokens - // strip BOM from first value - if(headers[0] && headers[0][0] == '\uFEFF') { - headers[0] = headers[0].substring(1) - } - } else try { - Fmessage fm = new Fmessage() - def dispatchStatus - headers.eachWithIndex { key, i -> - def value = tokens[i] - println "Processing cell value: $value for key '$key'" - if (key in standardFields) { - fm[standardFields[key]] = value - } else if (key == 'Message Date') { - fm.date = MESSAGE_DATE.parse(value) - } else if (key == 'Recipient Number') { - fm.addToDispatches(new Dispatch(dst:value)) - } else if(key == 'Message Type') { - fm.inbound = (value == 'Received') - } else if(key == 'Message Status') { - dispatchStatus = dispatchStatuses[value] - } else if (key == 'Source Mobile') { //version 2 import - fm.src = value - fm.inbound = true - importingVersionOne = false - } else if (key == 'Destination Mobile') { - value = value.replace("[","") - value.replace("]","").split(",").each{ - fm.addToDispatches(new Dispatch(dst:it)) - } - } else if (key == 'Date Created') { - fm.date = MESSAGE_DATE.parse(value) - } else if (key == 'Text') { - fm.text = value - } - } - if (fm.inbound) fm.dispatches = [] - else fm.dispatches.each { - it.status = dispatchStatus?: DispatchStatus.FAILED - if (dispatchStatus==DispatchStatus.SENT) it.dateSent = fm.date - } - -println "Is the message valid? ${fm.validate()}" -println "The errors are $fm.errors" - - Fmessage.withNewSession { - fm.save(failOnError:true) - } - ++savedCount - importingVersionOne ? saveMessagesIntoFolder("v1", fm) : saveMessagesIntoFolder("v2", fm) - } catch(Exception ex) { - ex.printStackTrace() - log.info message(code:'import.message.save.error'), ex - ++failedCount - } - } - flash.message = message(code: 'import.message.complete', args:[savedCount, failedCount]) - redirect controller:'settings', action:'general' - } - } - - private def getMessageFolder(name) { - Folder.findByName(name)?: new Folder(name:name).save(failOnError:true) - } - - private saveMessagesIntoFolder(version, fm){ - getMessageFolder("messages from "+version).addToMessages(fm) - } - - private def getGroupNames(csvValue) { - println "getGroupNames() : csvValue=$csvValue" - Set csvGroups = [] - csvValue.split("\\\\").each { gName -> - def longName - gName.split("/").each { shortName -> - csvGroups << shortName - longName = longName? "$longName-$shortName": shortName - csvGroups << longName - } - } - println "getGroupNames() : ${csvGroups - ''}" - return csvGroups - '' - } - - private def getGroups(groupNames) { - println "ImportController.getGroups() : $groupNames" - groupNames.collect { name -> - name = name.trim() - Group.findByName(name)?: new Group(name:name).save(failOnError:true) - } - } - - private def getFailedContactsFile() { - if(!params.jobId || params.jobId!=UUID.fromString(params.jobId).toString()) params.jobId = UUID.randomUUID().toString() - def f = new File(ResourceUtils.resourcePath, "import_contacts_${params.jobId}.csv") - f.deleteOnExit() - return f - } -} +package frontlinesms2 + +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.io.StringWriter + +import au.com.bytecode.opencsv.CSVWriter +import au.com.bytecode.opencsv.CSVParser +import ezvcard.* + +class ImportController extends ControllerUtils { + private final MESSAGE_DATE = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") + private final STANDARD_FIELDS = ['Name':'name', 'Mobile Number':'mobile', + 'Email':'email', 'Group(s)':'groups', 'Notes':'notes'] + private final CONTENT_TYPES = [csv:'text/csv', vcf:'text/vcard', vcfDepricatedMac:'text/directory', vcfDepricatedWindows:'text/x-vcard'] + + def contactImportService + def systemNotificationService + def appSettingsService + + def importData() { + if(request.exception) { + systemNotificationService.create(code:'import.maxfilesize.exceeded', args:[(grailsApplication.config.upload.maximum.size/(1024*1024) as int)]) + redirect controller:'settings', action:'porting' + return + } + log.info "ImportController.importData() :: params=$params" + if(params.data == 'messages') { + importMessages() + } else { + importContacts() + } + } + + private def importContacts() { + log.info "ImportController.importContacts() :: ENTRY" + if(params.reviewDone) { + ContactImportJob.triggerNow(['fileType':'csv', 'params':params, 'request':request]) + systemNotificationService.create(code:'importing.status.label', topic:'import.status') + redirect controller:'contact', action:'show' + return + } + switch(request.getFile('contactImportFile').contentType) { + case [CONTENT_TYPES.vcf, CONTENT_TYPES.vcfDepricatedMac, CONTENT_TYPES.vcfDepricatedWindows]: + ContactImportJob.triggerNow(['fileType':'vcf', 'params':params, 'request':request]) + systemNotificationService.create(code:'importing.status.label', topic:'import.status') + redirect controller:'contact', action:'show' + break + default: + prepareCsvReview() + } + println "ImportController.importContact() :: EXIT" + } + + private def prepareCsvReview() { + log.info "ImportController.prepareCsvReview() :: ENTRY" + def uploadedCSVFile = request.getFile('contactImportFile') + def csvAsNestedLists = [] + def headerRowSize + uploadedCSVFile.inputStream.toCsvReader([escapeChar:'�']).eachLine { tokens -> + if(!headerRowSize) { + headerRowSize = tokens.size() + } + if(tokens.size() == headerRowSize && tokens.find({it as Boolean})) { + csvAsNestedLists << tokens + } + } + + def csvEntryLimit = (appSettingsService.SP("csv.import.row.limit", "2000") as Integer) + def csvLimitReached = (csvAsNestedLists.size() > csvEntryLimit) + session.csvLimitReached = csvLimitReached + session.csvEntryLimit = csvEntryLimit + if(csvLimitReached) { + session.csvData = csvAsNestedLists.subList(0, csvEntryLimit + 1) + } else { + session.csvData = csvAsNestedLists + } + + redirect action:'reviewContacts' + return + } + + private def importMessages() { + def savedCount = 0 + def failedCount = 0 + def importingVersionOne = true + def uploadedCSVFile = request.getFile('contactImportFile') + if(uploadedCSVFile) { + def headers + def standardFields = ['Message Content':'text', 'Sender Number':'src'] + def dispatchStatuses = [Failed:DispatchStatus.FAILED, + Pending:DispatchStatus.PENDING, + Outbox:DispatchStatus.SENT, + Sent:DispatchStatus.SENT] + uploadedCSVFile.inputStream.toCsvReader([escapeChar:'�']).eachLine { tokens -> + if(!headers) { + headers = tokens + // strip BOM from first value + if(headers[0] && headers[0][0] == '\uFEFF') { + headers[0] = headers[0].substring(1) + } + } else try { + TextMessage fm = new TextMessage() + def dispatchStatus + headers.eachWithIndex { key, i -> + def value = tokens[i] + if (key in standardFields) { + fm[standardFields[key]] = value + } else if (key == 'Message Date') { + fm.date = MESSAGE_DATE.parse(value) + } else if (key == 'Recipient Number') { + fm.addToDispatches(new Dispatch(dst:value)) + } else if(key == 'Message Type') { + fm.inbound = (value == 'Received') + } else if(key == 'Message Status') { + dispatchStatus = dispatchStatuses[value] + } else if (key == 'Source Mobile') { //version 2 import + fm.src = value + fm.inbound = true + importingVersionOne = false + } else if (key == 'Destination Mobile') { + value = value.replace("[","") + value.replace("]","").split(",").each{ + fm.addToDispatches(new Dispatch(dst:it)) + } + } else if (key == 'Date Created') { + fm.date = MESSAGE_DATE.parse(value) + } else if (key == 'Text') { + fm.text = value + } + } + if (fm.inbound) fm.dispatches = [] + else fm.dispatches.each { + it.status = dispatchStatus?: DispatchStatus.FAILED + if (dispatchStatus==DispatchStatus.SENT) it.dateSent = fm.date + } + + TextMessage.withNewSession { + fm.save(failOnError:true) + } + ++savedCount + importingVersionOne ? saveMessagesIntoFolder("v1", fm) : saveMessagesIntoFolder("v2", fm) + } catch(Exception ex) { + ex.printStackTrace() + log.info message(code:'import.message.save.error'), ex + ++failedCount + } + } + flash.message = message(code: 'import.message.complete', args:[savedCount, failedCount]) + redirect controller:'settings', action:'porting' + } + } + + def failedContacts() { + response.setHeader("Content-disposition", "attachment; filename=failedContacts.${params.format}") + response.setHeader 'Content-Type', CONTENT_TYPES[params.format] + def failedContactFileContent = contactImportService.getFailedContactsByKey(params.key) + response.outputStream << failedContactFileContent + response.outputStream.flush() + } + + def reviewContacts() { + if(!session.csvData) { + redirect controller:'settings', action:'porting' + return + } + [csvData:session.csvData, csvLimitReached:session.csvLimitReached, csvEntryLimit:session.csvEntryLimit, recognisedTitles:STANDARD_FIELDS.keySet()] + } + + def contactWizard() { + render(template:"/contact/import_contacts") + } +} + diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/InlineEditableController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/InlineEditableController.groovy new file mode 100644 index 000000000..ab488d9f7 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/InlineEditableController.groovy @@ -0,0 +1,19 @@ +package frontlinesms2 + +import grails.converters.JSON + +class InlineEditableController extends ControllerUtils { + def grailsApplication + + def update() { + println params + def domainInstance = grailsApplication.getArtefact("Domain", params.domainclass)?.getClazz()?.get(params.instanceid) + domainInstance?."${params.field}" = params.value + if(domainInstance?.save()) { + render ([success:true, value: params.value] as JSON) + } + else { + render ([success:false, error: (domainInstance? domainInstance.errors.allErrors.collect { message(error:it) }.join(" ") : message(code:'domain.not.found'))] as JSON) + } + } +} diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/MessageController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/MessageController.groovy index 0a15ad40f..aad956d8b 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/MessageController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/MessageController.groovy @@ -9,9 +9,9 @@ class MessageController extends ControllerUtils { //> SERVICES def messageSendService - def fmessageInfoService + def textMessageInfoService def trashService - def fmessageService + def textMessageService //> INTERCEPTORS def bobInterceptor = { @@ -30,73 +30,50 @@ class MessageController extends ControllerUtils { redirect action:'inbox', params:params } - def newMessageCount() { - def section = params.messageSection - def messageCount - if(!params.ownerId && section != 'trash') { - if(section == 'pending') { - messageCount = Fmessage.countPending(params.failed) - } else { - messageCount = Fmessage."$section"(params.starred).count() - } - } else if(section == 'activity') { - def getSent = null - if(params.inbound) getSent = Boolean.parseBoolean(params.inbound) - messageCount = Activity.get(params.ownerId)?.getActivityMessages(params.starred, getSent)?.count() - } else if(section == 'folder') { - def getSent = null - if(params.inbound) getSent = Boolean.parseBoolean(params.inbound) - messageCount = Folder.get(params.ownerId)?.getFolderMessages(params.starred, getSent)?.count() - } else messageCount = 0 - render messageCount - } - def show() { - def messageInstance = Fmessage.get(params.messageId) + def interactionInstance = TextMessage.get(params.interactionId) def ownerInstance = MessageOwner.get(params?.ownerId) - messageInstance.read = true - messageInstance.save() + interactionInstance.read = true + interactionInstance.save() - def model = [messageInstance: messageInstance, + def model = [interactionInstance: interactionInstance, ownerInstance:ownerInstance, folderInstanceList: Folder.findAllByArchivedAndDeleted(viewingArchive, false), activityInstanceList: Activity.findAllByArchivedAndDeleted(viewingArchive, false), messageSection: params.messageSection] - render view:'/message/_single_message_details', model:model + render view:'/interaction/_single_interaction_details', model:model } def inbox() { - def messageInstanceList = Fmessage.inbox(params.starred, this.viewingArchive) - // check for flash message in parameters if there is none in flash.message - flash.message = flash.message?:params.flashMessage + def interactionInstanceList = TextMessage.inbox(params.starred, this.viewingArchive) render view:'../message/standard', - model:[messageInstanceList: messageInstanceList.list(params), + model:[interactionInstanceList: interactionInstanceList.list(params), messageSection:'inbox', - messageInstanceTotal: messageInstanceList.count()] << getShowModel() + interactionInstanceTotal: interactionInstanceList.count()] << getShowModel() } def sent() { - def messageInstanceList = Fmessage.sent(params.starred, this.viewingArchive) + def interactionInstanceList = TextMessage.sent(params.starred, this.viewingArchive) render view:'../message/standard', model:[messageSection:'sent', - messageInstanceList: messageInstanceList.list(params).unique(), - messageInstanceTotal: messageInstanceList.count()] << getShowModel() + interactionInstanceList: interactionInstanceList.list(params).unique(), + interactionInstanceTotal: interactionInstanceList.count()] << getShowModel() } def pending() { - render view:'standard', model:[messageInstanceList:Fmessage.listPending(params.failed, params), + render view:'standard', model:[interactionInstanceList:TextMessage.listPending(params.failed, params), messageSection:'pending', - messageInstanceTotal: Fmessage.countPending()] << getShowModel() + interactionInstanceTotal: TextMessage.countPending()] << getShowModel() } def trash() { def trashedObject def trashInstanceList - def messageInstanceList + def interactionInstanceList params.sort = params.sort?: 'date' if(params.id) { def setTrashInstance = { obj -> - if(obj.objectClass == "frontlinesms2.Fmessage") { - params.messageId = obj.objectId + if(obj.objectClass in ["frontlinesms2.TextMessage", "frontlinesms2.MissedCall"]) { + params.interactionId = obj.objectId } else { trashedObject = obj.object } @@ -104,38 +81,43 @@ class MessageController extends ControllerUtils { setTrashInstance(Trash.findById(params.id)) } if(params.starred) { - messageInstanceList = Fmessage.deleted(params.starred) + interactionInstanceList = TextMessage.deleted(params.starred) } else { if(params.sort == 'date') params.sort = 'dateCreated' trashInstanceList = Trash.list(params) } render view:'standard', model:[trashInstanceList: trashInstanceList, - messageInstanceList: messageInstanceList?.list(params), + interactionInstanceList: interactionInstanceList?.list(params), messageSection:'trash', - messageInstanceTotal: Trash.count(), - ownerInstance: trashedObject] << getShowModel() + interactionInstanceTotal: Trash.count(), + ownerInstance: trashedObject] << getShowModel() << (params.interactionId ? [interactionInstance: Interaction.get(params.interactionId)] : [:]) } def poll() { redirect(action: 'activity', params: params) } def webconnection() { redirect(action: 'activity', params: params) } + def autoforward() { redirect(action: 'activity', params: params) } + def customactivity() { redirect(action: 'activity', params: params) } def announcement() { redirect(action: 'activity', params: params) } def autoreply() { redirect(action: 'activity', params: params) } def subscription() { redirect(action: 'activity', params: params) } def activity() { def activityInstance = Activity.get(params.ownerId) if (activityInstance) { + if (params.starred == null) params.starred = false + if (params.failed == null) params.failed = false def getSent = params.containsKey("inbound") ? Boolean.parseBoolean(params.inbound) : null - def messageInstanceList = activityInstance.getActivityMessages(params.starred, getSent) + def interactionInstanceList = activityInstance.getActivityMessages(params.starred, getSent, params.stepId, params) def sentMessageCount = 0 def sentDispatchCount = 0 - Fmessage.findAllByMessageOwnerAndInbound(activityInstance, false).each { + TextMessage.findAllByMessageOwnerAndInbound(activityInstance, false).each { sentDispatchCount += it.dispatches.size() - sentMessageCount++ + sentMessageCount++ } render view:"/activity/${activityInstance.shortName}/show", - model:[messageInstanceList: messageInstanceList?.list(params), + model:[interactionInstanceList: interactionInstanceList, messageSection: params.messageSection?:'activity', - messageInstanceTotal: messageInstanceList?.count(), + interactionInstanceTotal: activityInstance.getMessageCount(params.starred, getSent), + stepInstance:Step.get(params.stepId), ownerInstance: activityInstance, viewingMessages: this.viewingArchive ? params.viewingMessages : null, pollResponse: activityInstance instanceof Poll ? activityInstance.responseStats as JSON : null, @@ -150,12 +132,12 @@ class MessageController extends ControllerUtils { def folder() { def folderInstance = Folder.get(params.ownerId) if (folderInstance) { + if (params.starred == null) params.starred = false def getSent = params.containsKey("inbound") ? Boolean.parseBoolean(params.inbound) : null - def messageInstanceList = folderInstance?.getFolderMessages(params.starred, getSent) - if (params.flashMessage) { flash.message = params.flashMessage } - render view:'../message/standard', model:[messageInstanceList: messageInstanceList.list(params), + def interactionInstanceList = folderInstance?.getFolderMessages(params.starred, getSent) + render view:'../message/standard', model:[interactionInstanceList: interactionInstanceList.list(params), messageSection:'folder', - messageInstanceTotal: messageInstanceList.count(), + interactionInstanceTotal: interactionInstanceList.count(), ownerInstance: folderInstance, viewingMessages: this.viewingArchive ? params.viewingMessages : null] << getShowModel() } else { @@ -165,10 +147,9 @@ class MessageController extends ControllerUtils { } def send() { - def fmessage = messageSendService.createOutgoingMessage(params) - messageSendService.send(fmessage) - flash.message = dispatchMessage 'queued', fmessage - render(text: flash.message) + def textMessage = messageSendService.createOutgoingMessage(params) + messageSendService.send(textMessage) + render(text: dispatchMessage('queued', textMessage)) } def retry() { @@ -186,52 +167,51 @@ class MessageController extends ControllerUtils { messages.each { m -> trashService.sendToTrash(m) } - params.flashMessage = dynamicMessage 'trashed', messages + flash.message = dynamicMessage 'trashed', messages if (params.messageSection == 'result') { redirect(controller:'search', action:'result', params: - [searchId:params.searchId, flashMessage:params.flashMessage]) + [searchId:params.searchId]) } else { - println "Forwarding to action: $params.messageSection" + log.info "Forwarding to action: $params.messageSection" redirect(controller:params.controller, action:params.messageSection, params: [ownerId:params.ownerId, starred:params.starred, - failed:params.failed, searchId:params.searchId, - flashMessage:params.flashMessage]) + failed:params.failed, searchId:params.searchId]) } } def archive() { def messages = getCheckedMessages().findAll { !it.messageOwner && !it.hasPending } - messages.each { messageInstance -> - messageInstance.archived = true - messageInstance.save() + messages.each { interactionInstance -> + interactionInstance.archived = true + interactionInstance.save() } - params.flashMessage = dynamicMessage 'archived', messages + flash.message = dynamicMessage 'archived', messages if(params.messageSection == 'result') { - redirect(controller: 'search', action: 'result', params: [searchId: params.searchId, flashMessage: params.flashMessage]) + redirect(controller: 'search', action: 'result', params: [searchId: params.searchId]) } else { - redirect(controller: params.controller, action: params.messageSection, params: [ownerId: params.ownerId, starred: params.starred, failed: params.failed, searchId: params.searchId, flashMessage: params.flashMessage]) + redirect(controller: params.controller, action: params.messageSection, params: [ownerId: params.ownerId, starred: params.starred, failed: params.failed, searchId: params.searchId]) } } def unarchive() { def messages = getCheckedMessages() - messages.each { messageInstance -> - if(!messageInstance.messageOwner) { - messageInstance.archived = false - messageInstance.save(failOnError: true) + messages.each { interactionInstance -> + if(!interactionInstance.messageOwner) { + interactionInstance.archived = false + interactionInstance.save(failOnError: true) } } - params.flashMessage = dynamicMessage 'unarchived', messages + flash.message = dynamicMessage 'unarchived', messages if(params.controller == 'search') - redirect(controller: 'search', action: 'result', params: [searchId: params.searchId, messageId: params.messageId, flashMessage:params.flashMessage]) + redirect(controller: 'search', action: 'result', params: [searchId: params.searchId, interactionId: params.interactionId]) else - redirect(controller: 'archive', action: params.messageSection, params: [ownerId: params.ownerId, flashMessage:params.flashMessage]) + redirect(controller: 'archive', action: params.messageSection, params: [ownerId: params.ownerId]) } def move() { def activity = params.messageSection == 'activity'? Activity.get(params.ownerId): null def messageList = getCheckedMessages() - fmessageService.move(messageList, activity, params) + textMessageService.move(messageList, activity, params) flash.message = dynamicMessage 'updated', messageList render 'OK' } @@ -239,8 +219,8 @@ class MessageController extends ControllerUtils { def changeResponse() { def responseInstance = PollResponse.get(params.responseId) def checkedMessages = getCheckedMessages() - checkedMessages.each { messageInstance -> - responseInstance.addToMessages(messageInstance) + checkedMessages.each { interactionInstance -> + responseInstance.addToMessages(interactionInstance) } responseInstance.poll.save() flash.message = dynamicMessage 'updated', checkedMessages @@ -248,17 +228,17 @@ class MessageController extends ControllerUtils { } def changeStarStatus() { - withFmessage { messageInstance -> - messageInstance.starred =! messageInstance.starred - messageInstance.save(failOnError: true) - Fmessage.get(params.messageId).messageOwner?.refresh() - params.remove('messageId') - render(text: messageInstance.starred ? "starred" : "unstarred") + withTextMessage { interactionInstance -> + interactionInstance.starred =! interactionInstance.starred + interactionInstance.save(failOnError: true) + TextMessage.get(params.interactionId).messageOwner?.refresh() + params.remove('interactionId') + render(text: interactionInstance.starred ? "starred" : "unstarred") } } def listRecipients() { - def message = Fmessage.get(params.messageId) + def message = TextMessage.get(params.interactionId) if(!message) { render text:'ERROR' return @@ -275,48 +255,44 @@ class MessageController extends ControllerUtils { trashService.emptyTrash() redirect action:'inbox' } - - def unreadMessageCount() { - render text:Fmessage.countUnreadMessages(), contentType:'text/plain' - } def sendMessageCount() { - render fmessageInfoService.getMessageInfos(params.message) as JSON + render textMessageInfoService.getMessageInfos(params.message) as JSON } //> PRIVATE HELPERS boolean isViewingArchive() { params.controller=='archive' } - private def withFmessage = withDomainObject Fmessage, { params.messageId } + private def withTextMessage = withDomainObject TextMessage, { params.interactionId } - private def getShowModel(messageInstanceList) { - def messageInstance = params.messageId? Fmessage.get(params.messageId): null - messageInstance?.read = true - messageInstance?.save() + private def getShowModel() { + def interactionInstance = params.interactionId? TextMessage.get(params.interactionId): null + interactionInstance?.read = true + interactionInstance?.save() def checkedMessageCount = getCheckedMessageList().size() - [messageInstance: messageInstance, + [interactionInstance: interactionInstance, checkedMessageCount: checkedMessageCount, activityInstanceList: Activity.findAllByArchivedAndDeleted(viewingArchive, false), folderInstanceList: Folder.findAllByArchivedAndDeleted(viewingArchive, false), - messageCount: Fmessage.countAllMessages(params), - hasFailedMessages: Fmessage.hasFailedMessages(), - failedDispatchCount: messageInstance?.hasFailed ? Dispatch.findAllByMessageAndStatus(messageInstance, DispatchStatus.FAILED).size() : 0] + messageCount: TextMessage.countAllMessages(), + hasFailedMessages: TextMessage.hasFailedMessages(), + failedDispatchCount: interactionInstance?.hasFailed ? Dispatch.findAllByMessageAndStatus(interactionInstance, DispatchStatus.FAILED).size() : 0] } private def getCheckedMessages() { - return Fmessage.getAll(getCheckedMessageList()) - null + return TextMessage.getAll(getCheckedMessageList()) - null } private def getCheckedMessageList() { - def checked = params['message-select']?: params.messageId?: [] + def checked = params['interaction-select']?: params.interactionId?: [] if(checked instanceof String) checked = checked.split(/\D+/) - '' if(checked instanceof Number) checked = [checked] if(checked.class.isArray()) checked = checked as List return checked } - private def dispatchMessage(String code, Fmessage m) { + private def dispatchMessage(String code, TextMessage m) { def args code = 'fmessage.' + code if(m.dispatches.size() == 1) { diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/MissedCallController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/MissedCallController.groovy new file mode 100644 index 000000000..0bdecec09 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/MissedCallController.groovy @@ -0,0 +1,177 @@ +package frontlinesms2 + +import grails.converters.* + + +class MissedCallController extends ControllerUtils { +//> CONSTANTS + static allowedMethods = [save: "POST", update: "POST", delete: "POST", archive: "POST"] + +//> SERVICES + def trashService + +//> INTERCEPTORS + def bobInterceptor = { + params.sort = params.sort ?: 'date' + params.order = params.order ?: 'desc' + params.starred = params.starred ? params.starred.toBoolean() : false + params.failed = params.failed ? params.failed.toBoolean() : false + params.max = params.max?: grailsApplication.config.grails.views.pagination.max + params.offset = params.offset ?: 0 + return true + } + def beforeInterceptor = [except:'index', action:bobInterceptor] + +//> ACTIONS + def index() { + redirect action:'inbox', params:params + } + + def missedCalls() { + redirect action:'inbox', params:params + } + + def show() { + def interactionInstance = MissedCall.get(params.interactionId) + interactionInstance.read = true + interactionInstance.save() + + def model = [interactionInstance: interactionInstance, + folderInstanceList: Folder.findAllByArchivedAndDeleted(false, false), + activityInstanceList: Activity.findAllByArchivedAndDeleted(false, false), + missedCallSection: 'inbox'] // TODO correct this when missedCalls can enter other sections + render view:'/missedCall/_single_interaction_details', model:model + } + + def inbox() { + def interactionInstanceList = MissedCall.inbox(params.starred) + render view:'../missedCall/standard', + model:[interactionInstanceList: interactionInstanceList.list(params), + messageSection:'missedCalls', + interactionInstanceTotal: interactionInstanceList.count()] << getShowModel() + } + + def trash() { + def trashedObject + def trashInstanceList + def interactionInstanceList + params.sort = params.sort?: 'date' + if(params.id) { + def setTrashInstance = { obj -> + if(obj.objectClass == "frontlinesms2.MissedCall") { + params.interactionId = obj.objectId + } else { + trashedObject = obj.object + } + } + setTrashInstance(Trash.findById(params.id)) + } + if(params.starred) { + interactionInstanceList = MissedCall.deleted(params.starred) + } else { + if(params.sort == 'date') params.sort = 'dateCreated' + trashInstanceList = Trash.list(params) + } + render view:'standard', model:[trashInstanceList: trashInstanceList, + interactionInstanceList: interactionInstanceList?.list(params), + missedCallSection:'trash', + interactionInstanceTotal: Trash.count(), + ownerInstance: trashedObject] << getShowModel() + } + + def delete() { + def missedCalls = getCheckedMissedCalls() + missedCalls.each { m -> + m.delete() + } + flash.message = dynamicMessage 'deleted', missedCalls + redirect(controller:params.controller, action:params.missedCallSection, params: + [ownerId:params.ownerId, starred:params.starred, + failed:params.failed, searchId:params.searchId]) + } + + def archive() { + def missedCalls = getCheckedMissedCalls().findAll { !it.missedCallOwner && !it.hasPending } + missedCalls.each { interactionInstance -> + interactionInstance.archived = true + interactionInstance.save() + } + flash.message = dynamicMessage 'archived', missedCalls + if(params.missedCallSection == 'result') { + redirect(controller: 'search', action: 'result', params: [searchId: params.searchId]) + } else { + redirect(controller: params.controller, action: params.missedCallSection, params: [ownerId: params.ownerId, starred: params.starred, failed: params.failed, searchId: params.searchId]) + } + } + + def unarchive() { + def missedCalls = getCheckedMissedCalls() + missedCalls.each { interactionInstance -> + if(!interactionInstance.missedCallOwner) { + interactionInstance.archived = false + interactionInstance.save(failOnError: true) + } + } + flash.message = dynamicMessage 'unarchived', missedCalls + if(params.controller == 'search') + redirect(controller: 'search', action: 'result', params: [searchId: params.searchId, interactionId: params.interactionId]) + else + redirect(controller: 'archive', action: params.missedCallSection, params: [ownerId: params.ownerId]) + } + + def changeStarStatus() { + withMissedCall { interactionInstance -> + interactionInstance.starred =! interactionInstance.starred + interactionInstance.save(failOnError: true) + params.remove('interactionId') + render(text: interactionInstance.starred ? "starred" : "unstarred") + } + } + + private def withMissedCall = withDomainObject MissedCall, { params.interactionId } + + private def getShowModel() { + def interactionInstance = params.interactionId? MissedCall.get(params.interactionId): null + interactionInstance?.read = true + interactionInstance?.save() + + def checkedMissedCallCount = getCheckedMissedCallList().size() + [interactionInstance: interactionInstance, + checkedMissedCallCount: checkedMissedCallCount, + activityInstanceList: Activity.findAllByArchivedAndDeleted(false, false), + folderInstanceList: Folder.findAllByArchivedAndDeleted(false, false), + messageCount: TextMessage.countAllMessages(), + missedCallCount: MissedCall.countAllMissedCalls()] + } + + private def getCheckedMissedCalls() { + return MissedCall.getAll(getCheckedMissedCallList()) - null + } + + private def getCheckedMissedCallList() { + def checked = params['interaction-select']?: params.interactionId?: [] + if(checked instanceof String) checked = checked.split(/\D+/) - '' + if(checked instanceof Number) checked = [checked] + if(checked.class.isArray()) checked = checked as List + return checked + } + + private def dynamicMessage(String code, def list) { + def count = list.size() + if(count == 1) defaultMessage code + else pluralMessage code, count + } + + private def defaultMessage(String code, Object... args=[]) { + def messageName = message code:'missedCall.label' + return message(code:'default.' + code, + args:[messageName] + args) + } + + private def pluralMessage(String code, count, Object... args=[]) { + def messageName = message code:'missedCall.label.multiple', args:[count] + return message(code:'default.' + code + '.multiple', + args:[messageName] + args) + } +} + diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/PollController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/PollController.groovy index 3d5960e19..46a4721b0 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/PollController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/PollController.groovy @@ -1,20 +1,18 @@ package frontlinesms2 -import grails.converters.JSON - - class PollController extends ActivityController { def pollService def save() { + params.keywords = params.topLevelKeyword?: "$params.keywordsA,$params.keywordsB,$params.keywordsC,$params.keywordsD,$params.keywordsE" withPoll { poll -> - doSave('poll', pollService, poll) + doSave(pollService, poll) } } def sendReply() { def poll = Poll.get(params.ownerId) - def incomingMessage = Fmessage.get(params.messageId) + def incomingMessage = TextMessage.get(params.messageId) if(poll.autoreplyText) { params.addresses = incomingMessage.src params.messageText = poll.autoreplyText @@ -26,12 +24,6 @@ class PollController extends ActivityController { render '' } - def pollStats() { - withPoll { pollInstance -> - render (pollInstance.responseStats as JSON) - } - } - private def withPoll = withDomainObject Poll, { params.ownerId } } diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/QuickMessageController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/QuickMessageController.groovy index 372f855ae..148a24d71 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/QuickMessageController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/QuickMessageController.groovy @@ -1,40 +1,37 @@ package frontlinesms2 class QuickMessageController extends ControllerUtils { + private static final CONTACT_ID_PATTERN = /^contact-(\d+)$/ + def create() { - if( params.recipients?.contains(',')) { - def recipientList = [] - params.recipients.tokenize(',').each { - def msg = Fmessage.findById(it) - if (msg.inbound) - { + def groupList = params.groupList? Group.getAll(params.groupList.split(',').flatten().collect{ it as Long }): [] + def recipientList = [] + if(params.messageIds?.contains(',')) { + params.messageIds.tokenize(',').each { + def msg = Interaction.findById(it) + if (msg.inbound) { recipientList << msg.src - } - else - { - msg.dispatches.each{ recipientList << it.dst } + } else { + recipientList += msg.dispatches*.dst } } - params.recipients = recipientList.unique() + recipientList = recipientList.unique() + } else if(params.contactId) { + recipientList << Contact.get(params.contactId).mobile } - def recipients = params.recipients ? [params.recipients].flatten() : [] - def recipientName = recipients.size() == 1 ? (Contact.findByMobile(recipients[0])?.name ?: recipients[0]) : "" - def contacts = Contact.list(sort: "name") - def configureTabs = params.configureTabs ? configTabs(params.configureTabs): ['tabs-1', 'tabs-2', 'tabs-3', 'tabs-4'] - def groupList = Group.getGroupDetails() + SmartGroup.getGroupDetails() - def nonContactRecipients = [] - recipients.each { if (!Contact.findByMobile(it)) nonContactRecipients << it } - [contactList: contacts, - configureTabs: configureTabs, - groupList:groupList, - recipients:recipients, - nonContactRecipients:nonContactRecipients, - recipientName: recipientName, - messageText: params.messageText ? params.messageText : [], - nonExistingRecipients:recipients - contacts*.getMobile() - contacts*.getEmail()] + recipientList = recipientList.flatten() + def recipientName = recipientList.size() == 1 ? (Contact.findByMobile(recipientList[0])?.name?: recipientList[0]): '' + def configureTabs = params.configureTabs? configTabs(params.configureTabs): ['tabs-1', 'tabs-2', 'tabs-3', 'tabs-4'] + [configureTabs:configureTabs, + addresses:recipientList, + groups:groupList, + recipientCount:recipientList.size(), + recipientName:recipientName, + messageText:params.messageText?:''] } private def configTabs(configTabs) { return configTabs.tokenize(",")*.trim() } } + diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/SearchController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/SearchController.groovy index da07172e6..796124bd9 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/SearchController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/SearchController.groovy @@ -1,9 +1,13 @@ - package frontlinesms2 import grails.util.GrailsConfig +import grails.converters.JSON class SearchController extends MessageController { + def recipientLookupService + def contactSearchService + def textMessageService + def beforeInterceptor = { params.offset = params.offset ?: 0 params.max = params.max ?: GrailsConfig.config.grails.views.pagination.max @@ -34,6 +38,7 @@ class SearchController extends MessageController { searchInstance.startDate = params.startDate ?: null searchInstance.endDate = params.endDate ?: null searchInstance.customFields = [:] + searchInstance.starredOnly = params.starred?: false CustomField.getAllUniquelyNamed().each { customFieldName -> if(params[customFieldName]) searchInstance.customFields[customFieldName] = params[customFieldName] @@ -41,26 +46,27 @@ class SearchController extends MessageController { searchInstance.save(failOnError:true, flush:true) } - def rawSearchResults = Fmessage.search(search) - int offset = params.offset?.toInteger()?:0 - int max = params.max?.toInteger()?:50 - def checkedMessageCount = params.checkedMessageList?.tokenize(',')?.size() - flash.message = params.flashMessage + def searchResultsList = textMessageService.search(search, params) + [searchDescription:getSearchDescription(search), search:search, checkedMessageCount:params.checkedMessageList?.tokenize(',')?.size(), - messageInstanceList:rawSearchResults.listDistinct(sort:'date', order:'desc', offset:offset, max:max), - messageInstanceTotal:rawSearchResults.count()] << show() << no_search() + interactionInstanceList:searchResultsList.interactionInstanceList, + interactionInstanceTotal:searchResultsList.interactionInstanceTotal] << show() << no_search() } def show() { - def messageInstance = params.messageId ? Fmessage.get(params.messageId.toLong()) : null - if (messageInstance && !messageInstance.read) { - messageInstance.read = true - messageInstance.save() + def interactionInstance = params.interactionId ? TextMessage.get(params.interactionId.toLong()) : null + if (interactionInstance && !interactionInstance.read) { + interactionInstance.read = true + interactionInstance.save() } - [messageInstance: messageInstance] + [interactionInstance: interactionInstance] } + def contactSearch() { + render(contentType: 'text/json') { recipientLookupService.lookup(params) } + } + private def getSearchDescription(search) { String searchDescriptor = message(code: 'searchdescriptor.searching') if(search.searchString) { @@ -117,10 +123,17 @@ class SearchController extends MessageController { search.customFields.each() { customFieldName, val -> params[customFieldName] = val } + params.starred = search.starredOnly } else { search = new Search(name: 'TempSearchObject') } c.call(search) } + + def recipientCount() { + def recipients = [params.recipients].flatten() + def addresses = recipientLookupService.getAddressesFromRecipientList(recipients) + render([recipientCount: addresses.size()] as JSON) + } } diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/SettingsController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/SettingsController.groovy index 3b01bf50b..6978ada18 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/SettingsController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/SettingsController.groovy @@ -1,6 +1,5 @@ package frontlinesms2 - class SettingsController extends ControllerUtils { def i18nUtilService def appSettingsService @@ -26,17 +25,23 @@ class SettingsController extends ControllerUtils { } def general() { - def enabledAuthentication = appSettingsService.get("auth.basic.enabled") - def username = new String(appSettingsService.get("auth.basic.username").decodeBase64()) - def password = new String(appSettingsService.get("auth.basic.password").decodeBase64()) + def authEnabled = appSettingsService.get("auth.basic.enabled") + def username = appSettingsService.get("auth.basic.username")? new String(appSettingsService.get("auth.basic.username").decodeBase64()):'' + def password = appSettingsService.get("auth.basic.password")? new String(appSettingsService.get("auth.basic.password").decodeBase64()):'' [currentLanguage:i18nUtilService.getCurrentLanguage(request), - enabledAuthentication:enabledAuthentication, + authEnabled:authEnabled, username:username, password:password, languageList:i18nUtilService.allTranslations] } + def porting() { + [failedContacts:flash.failedContacts, + numberOfFailedLines:flash.numberOfFailedLines, + failedContactsFormat:flash.failedContactsFormat] + } + def selectLocale() { i18nUtilService.setLocale(request, response, params.language?:'en') redirect view:'general' @@ -44,19 +49,17 @@ class SettingsController extends ControllerUtils { def basicAuth() { if(appSettingsService.get("auth.basic.enabled") && appSettingsService.get("auth.basic.username") && appSettingsService.get("auth.basic.password")) { - appSettingsService.set('auth.basic.enabled', params.enabledAuthentication) + appSettingsService.set('auth.basic.enabled', params.enabled) } if(params.password && params.password == params.confirmPassword) { - appSettingsService.set('auth.basic.enabled', params.enabledAuthentication) + appSettingsService.set('auth.basic.enabled', params.enabled) appSettingsService.set('auth.basic.username', params.username.bytes.encodeBase64().toString()) appSettingsService.set('auth.basic.password', params.password.bytes.encodeBase64().toString()) } else if(params.password != params.confirmPassword) { - flash.message = message(code:"basic.authentication.password.mismatch") + flash.message = message(code:"auth.basic.password.mismatch") } // render general rather than redirecting so that auth is not immediately asked for render view:'general', model:general() } - - private def withFconnection = withDomainObject Fconnection, { params.id }, { render(view:'show_connections', model: [fconnectionInstanceTotal: 0]) } } diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/SmartGroupController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/SmartGroupController.groovy index a6d52b92e..e277a8261 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/SmartGroupController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/SmartGroupController.groovy @@ -42,19 +42,19 @@ class SmartGroupController extends ControllerUtils { def edit() { def smartGroupInstance = SmartGroup.get(params.id) - def smartGroupRuleFields = getSmartGroupRuleFields() def currentRules = [:] - - for(def prop in smartGroupRuleFields) { - if(smartGroupInstance."$prop") - currentRules."$prop" = smartGroupInstance."$prop" - } + + SmartGroup.configFields.each { field -> + if(smartGroupInstance."$field") + currentRules."$field" = smartGroupInstance."$field" + } + def customFieldNames = CustomField.allUniquelyNamed render view: "../smartGroup/create", model: [smartGroupInstance:smartGroupInstance, currentRules:currentRules, - fieldNames:[message(code: 'contact.phonenumber.label') , message(code: 'contact.name.label'), message(code: 'contact.email.label'), message(code: 'contact.notes.label')]+customFieldNames, - fieldIds:['mobile', 'contactName', 'email', 'notes']+customFieldNames.collect { CUSTOM_FIELD_ID_PREFIX+it }] + fieldNames:[message(code: 'contact.phonenumber.label') , message(code: 'contact.name.label'), message(code: 'contact.email.label'), message(code: 'contact.notes.label')] + customFieldNames, + fieldIds: SmartGroup.configFields+customFieldNames.collect { CUSTOM_FIELD_ID_PREFIX+it }] } def confirmDelete() { @@ -62,11 +62,11 @@ class SmartGroupController extends ControllerUtils { } def delete() { - if (SmartGroup.get(params.id)?.delete(flush: true)) - flash.message = "${message(code: 'default.deleted.message', args: [message(code: 'smartgroup.label', default: 'SmartGroup'), ''])}" - else - flash.message = message(code: 'flash.smartgroup.delete.unable') - redirect(controller: "contact") + SmartGroup.withTransaction { + SmartGroup.get(params.id)?.delete(flush: true) + } + flash.message = "${message(code: 'default.deleted.message', args: [message(code: 'smartgroup.label', default: 'SmartGroup'), ''])}" + redirect(controller: "contact", action:"show") } private def getRuleText() { @@ -76,7 +76,7 @@ class SmartGroupController extends ControllerUtils { private def getRuleField(i) { def f = params['rule-field'] - println "field $f" + log.info "field $f" if(f instanceof String[]) return f[i] else { assert i == 0 @@ -98,8 +98,7 @@ class SmartGroupController extends ControllerUtils { } private def removeSmartGroupRules(smartGroupInstance) { - def smartGroupRuleFields = getSmartGroupRuleFields() - def fieldsToNullify = smartGroupRuleFields - params['rule-field'] + def fieldsToNullify = SmartGroup.configFields - params['rule-field'] for(def field in fieldsToNullify) { if(field == "customFields") { smartGroupInstance.customFields.each {it.smartGroup = null} @@ -109,9 +108,5 @@ class SmartGroupController extends ControllerUtils { } } - private def getSmartGroupRuleFields() { - def smartGroupRuleFields = (new DefaultGrailsDomainClass(SmartGroup.class)).persistentProperties*.name - "name" - smartGroupRuleFields - } } diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/StatusController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/StatusController.groovy index 6af4d6211..5cadd50e5 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/StatusController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/StatusController.groovy @@ -6,12 +6,6 @@ class StatusController extends ControllerUtils { def index() { redirect action:'show', params:params } - - def trafficLightIndicator() { - def connections = Fconnection.list() - def color = (connections && connections.status.any {(it == ConnectionStatus.CONNECTED)}) ? 'green' : 'red' - render text:color, contentType:'text/plain' - } def show() { [connectionInstanceList: Fconnection.list(), @@ -25,10 +19,6 @@ class StatusController extends ControllerUtils { redirect action:'show' } - def listDetected() { - render template:'device_detection', model:[detectedDevices:deviceDetectionService.detected] - } - def resetDetection() { deviceDetectionService.reset() redirect action:'index' @@ -41,7 +31,7 @@ class StatusController extends ControllerUtils { params.messageOwner = activityInstance params.groupInstance = params.groupId ? Group.get(params.groupId) : null params.messageStatus = params.messageStatus?.tokenize(",")*.trim() - def messageStats = Fmessage.getMessageStats(params) // TODO consider changing the output of this method to match the data we actually want + def messageStats = TextMessage.getMessageStats(params) // TODO consider changing the output of this method to match the data we actually want [messageStats: [xdata: messageStats.keySet().collect{"'${it}'"}, sent: messageStats.values()*.sent, received: messageStats.values()*.received]] @@ -56,5 +46,5 @@ class StatusController extends ControllerUtils { activityInstanceList: Activity.findAllByDeleted(false), folderInstanceList: Folder.findAllByDeleted(false)] } - } + diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/SubscriptionController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/SubscriptionController.groovy index 39e5bd1b5..fe4674e95 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/SubscriptionController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/SubscriptionController.groovy @@ -1,22 +1,17 @@ package frontlinesms2 -import grails.converters.JSON - class SubscriptionController extends ActivityController { def subscriptionService def create() { - def groupList = Group.getAll() - [contactList:Contact.list(), groupList:groupList] } def edit() { withActivity { activityInstance -> - def groupList = Group.getGroupDetails() + SmartGroup.getGroupDetails() def activityType = activityInstance.shortName - render view:"../$activityType/create", model:[contactList:Contact.list(), - groupList:groupList, - activityInstanceToEdit:activityInstance] + render view:"../$activityType/create", model: [ + activityInstanceToEdit:activityInstance + ] } } @@ -51,8 +46,10 @@ class SubscriptionController extends ActivityController { } def save() { + //TODO Should use the withDefault subscription closure def subscriptionInstance = Subscription.get(params.ownerId)?: new Subscription() - doSave('subscription', subscriptionService, subscriptionInstance) + params.keywords = (params.topLevelKeywords?.trim()?.length() > 0) ? params.topLevelKeywords:("${params.joinKeywords},${params.leaveKeywords}") + doSave(subscriptionService, subscriptionInstance) } def categoriseSubscriptionPopup() { @@ -65,11 +62,11 @@ class SubscriptionController extends ActivityController { } private def getCheckedMessages() { - return Fmessage.getAll(getCheckedMessageList()) - null + return TextMessage.getAll(getCheckedMessageList()) - null } private def getCheckedMessageList() { - def checked = params.messagesList?: params.messageId?: [] + def checked = params['interaction-select']?: params.interactionId?: [] if(checked instanceof String) checked = checked.split(/\D+/) - '' if(checked instanceof Number) checked = [checked] if(checked.class.isArray()) checked = checked as List diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/SystemNotificationController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/SystemNotificationController.groovy index a19974276..b4245d1d1 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/SystemNotificationController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/SystemNotificationController.groovy @@ -11,12 +11,6 @@ class SystemNotificationController extends ControllerUtils { render text: message(code: 'system.notification.ok') } } - - def list() { - def notifications = SystemNotification.findAllByRead(false) - def data = notifications.collectEntries { [it.id, it.text] } - render data as JSON - } private def withNotification = withDomainObject SystemNotification } diff --git a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/WebconnectionController.groovy b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/WebconnectionController.groovy index aedba833f..ca1bb3c42 100644 --- a/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/WebconnectionController.groovy +++ b/plugins/frontlinesms-core/grails-app/controllers/frontlinesms2/WebconnectionController.groovy @@ -2,7 +2,6 @@ package frontlinesms2 import grails.converters.JSON - class WebconnectionController extends ActivityController { static final def WEB_CONNECTION_TYPE_MAP = [generic:GenericWebconnection, ushahidi:UshahidiWebconnection] @@ -12,35 +11,29 @@ class WebconnectionController extends ActivityController { def create() {} def save() { - def webconnectionInstance - Class clazz = WebconnectionController.WEB_CONNECTION_TYPE_MAP[params.webconnectionType] - if(params.ownerId) { - webconnectionInstance = clazz.get(params.ownerId) - } else { - webconnectionInstance = clazz.newInstance() + if(params.activityId) params.ownerId = params.activityId + withWebconnection { webconnectionInstance -> + doSave(webconnectionService, webconnectionInstance) } - doSave('webconnection', webconnectionService, webconnectionInstance) } def config() { - def activityInstanceToEdit - if(params.ownerId) activityInstanceToEdit = WEB_CONNECTION_TYPE_MAP[params.imp].get(params.ownerId) - else activityInstanceToEdit = WEB_CONNECTION_TYPE_MAP[params.imp].newInstance() - def responseMap = ['config', 'scripts', 'confirm'].collectEntries { - [it, g.render(template:"/webconnection/$params.imp/$it", model:[activityInstanceToEdit:activityInstanceToEdit])] + withWebconnection { activityInstanceToEdit -> + def responseMap = ['config', 'scripts', 'confirm'].collectEntries { + [it, fsms.render(template:"/webconnection/$params.imp/$it", model:[activityInstanceToEdit:activityInstanceToEdit])] + } + render responseMap as JSON } - render responseMap as JSON } - private def renderJsonErrors(webconnectionInstance) { - def errorMessages = webconnectionInstance.errors.allErrors.collect { message(error:it) }.join("\n") - withFormat { - json { - render([ok:false, text:errorMessages] as JSON) - } + def retryFailed() { + withWebconnection { c -> + webconnectionService.retryFailed(c) + flash.message = g.message(code: 'webconnection.failed.retried') + redirect action:'show', params:[ownerId:c.id] } } - private def withWebconnection = withDomainObject Webconnection + private def withWebconnection = withDomainObject({ WEB_CONNECTION_TYPE_MAP[params.webconnectionType?:params.imp]?: Webconnection }, { params.ownerId }) } diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Activity.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Activity.groovy index 01f2de20a..0c8298a64 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Activity.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Activity.groovy @@ -3,7 +3,7 @@ package frontlinesms2 abstract class Activity extends MessageOwner { //> STATIC PROPERTIES static boolean editable = { true } - static def implementations = [Announcement, Autoreply, Poll, Subscription, Webconnection, Autoforward] + static def implementations = [Announcement, Autoreply, Poll, Subscription, Webconnection, Autoforward, CustomActivity] protected static final def NAME_VALIDATOR = { activityDomainClass -> return { val, obj -> if(obj?.deleted || obj?.archived) return true @@ -30,15 +30,22 @@ abstract class Activity extends MessageOwner { static constraints = { sentMessageText(nullable:true) + keywords(validator: { val, obj -> + !val || val?.every { it.validate() } + }) } //> ACCESSORS - def getActivityMessages(getOnlyStarred=false, getSent=null) { - Fmessage.owned(this, getOnlyStarred, getSent) + def getActivityMessages(getOnlyStarred=false, getSent=null, stepId=null, params=null) { + TextMessage.owned(this, getOnlyStarred, getSent).list(params?:[:]) + } + + def getMessageCount(getOnlyStarred=false, getSent=null) { + TextMessage.owned(this, getOnlyStarred, getSent).count() } def getLiveMessageCount() { - def m = Fmessage.findAllByMessageOwnerAndIsDeleted(this, false) + def m = TextMessage.findAllByMessageOwnerAndIsDeleted(this, false) m ? m.size() : 0 } @@ -60,7 +67,11 @@ abstract class Activity extends MessageOwner { this.messages*.isDeleted = false } - def processKeyword(Fmessage message, Keyword match) {} + def processKeyword(TextMessage message, Keyword match) { + message.setMessageDetail(this, "") + this.addToMessages(message) + this.save(failOnError:true) + } /** * Activcate this activity. If it is already activated, this method should @@ -70,11 +81,9 @@ abstract class Activity extends MessageOwner { def deactivate() {} - private def logFail(c, ex) { - ex.printStackTrace() - log.warn("Error creating routes of webconnection with id $c?.id", ex) - LogEntry.log("Error creating routes to webconnection with name ${c?.name?: c?.id}") - //createSystemNotification('connection.route.failNotification', [c.id, c?.name?:c?.id], ex) + def move(messageInstance) { + messageInstance.messageOwner?.removeFromMessages(messageInstance)?.save(failOnError:true) + this.processKeyword(messageInstance, null) } } diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Autoforward.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Autoforward.groovy index a801f752c..0ecb58062 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Autoforward.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Autoforward.groovy @@ -3,15 +3,9 @@ package frontlinesms2 class Autoforward extends Activity { //> CONSTANTS static def shortName = 'autoforward' - private static def RECIPIENT_VALIDATOR = { val, obj -> - println "RECIPIENT_VALIDATOR:: obj=$obj val=$val" - def valid = val || obj.contacts || obj.groups || obj.smartGroups - println "Valid: $valid" - return valid - } //> SERVICES - def messageSendService + def autoforwardService //> PROPERTIES static hasMany = [contacts:Contact, groups:Group, smartGroups:SmartGroup] @@ -19,32 +13,35 @@ class Autoforward extends Activity { //> DOMAIN SETUP static constraints = { name blank:false, maxSize:255, validator:NAME_VALIDATOR(Autoforward) - contacts validator:RECIPIENT_VALIDATOR - groups validator:RECIPIENT_VALIDATOR - smartGroups validator:RECIPIENT_VALIDATOR sentMessageText blank:false } //> ACCESSORS int getRecipientCount() { - (contacts? contacts.size(): 0) + - (groups? (groups.collect { it.members?.size()?:0 }?.sum()): 0) + - (smartGroups? (smartGroups.collect { it.members?.size()?:0 }?.sum()): 0) + def numbers = [] + contacts.each { numbers << it.mobile } + // FIXME please fix spaces around braces + groups.each { it.members.each { numbers << it.mobile }} + smartGroups.each { it.members.each { numbers << it.mobile }} + numbers.unique().size() } //> PROCESS METHODS - def processKeyword(Fmessage message, Keyword matchedKeyword) { - println "#####Mocked OwnerDetail ## $message.ownerDetail" - println "#####Mocked id ## $message.id" - def m = messageSendService.createOutgoingMessage([contacts:contacts, groups:groups?:[] + smartGroups?:[], messageText:sentMessageText]) - println "#####Mocked OutgoingMessage ## $m.id" - m.ownerDetail = message.id - this.addToMessages(m) + def processKeyword(TextMessage message, Keyword matchedKeyword) { this.addToMessages(message) - m.save(failOnError:true) - println "############# OwnerDetail of OutgoingMessage ## $m ####### $m.ownerDetail" - messageSendService.send(m) this.save(failOnError:true) + // FIXME please fix spaces around braces + if(addressesAvailable()){ + autoforwardService.doForward(this, message) + } + } + + // FIXME declare this as `boolean` return type, remove `.size() > 0` check, rename to follow standard naming conventions + // FIXME can also be simplified by ORing results together + // FIXME this method also looks like it could be rewritten: `return getRecipientCount()` + private def addressesAvailable() { + println "## All Contacts ## ${((contacts?:[] + groups*.members?:[] + smartGroups*.members?:[]).flatten() - null)}" + ((contacts?:[] + groups*.members?:[] + smartGroups*.members?:[]).flatten() - null).size() > 0 } } diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Autoreply.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Autoreply.groovy index ac7c2ba9b..5b5fff57b 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Autoreply.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Autoreply.groovy @@ -18,19 +18,13 @@ class Autoreply extends Activity { } //> SERVICES - def messageSendService + def autoreplyService //> PROCESS METHODS - def processKeyword(Fmessage message, Keyword matchedKeyword) { - def params = [:] - params.addresses = message.src - params.messageText = autoreplyText - addToMessages(message) - def outgoingMessage = messageSendService.createOutgoingMessage(params) - addToMessages(outgoingMessage) - messageSendService.send(outgoingMessage) - save() - println "Autoreply message sent to ${message.src}" + def processKeyword(TextMessage message, Keyword matchedKeyword) { + this.addToMessages(message) + autoreplyService.doReply(this, message) + this.save(failOnError:true) } } diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/ClickatellFconnection.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/ClickatellFconnection.groovy index 712355281..cbe725d73 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/ClickatellFconnection.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/ClickatellFconnection.groovy @@ -9,17 +9,31 @@ import frontlinesms2.camel.exception.* class ClickatellFconnection extends Fconnection { private static final String CLICKATELL_URL = 'http://api.clickatell.com/http/sendmsg?' - static final configFields = ['name', 'apiId', 'username', 'password'] + static final configFields = ['info-local':['name'], 'info-clickatell':['apiId', 'username', 'password'], 'sendToUsa':['fromNumber']] static final defaultValues = [] static String getShortName() { 'clickatell' } String apiId String username String password // FIXME maybe encode this rather than storing plaintext + boolean sendToUsa + String fromNumber + boolean sendEnabled = true + boolean receiveEnabled = false - static passwords = ['password'] + static constraints = { + apiId blank:false + username blank:false + password blank:false + fromNumber(nullable: true, validator: { val, obj -> + return !obj.sendToUsa || val + }) + } + + static passwords = [] static mapping = { + tablePerHierarchy false password column: 'clickatell_password' } @@ -29,7 +43,7 @@ class ClickatellFconnection extends Fconnection { List getRouteDefinitions() { return [from("seda:out-${ClickatellFconnection.this.id}") .onException(AuthenticationException, InvalidApiIdException, InsufficientCreditException) - .handled(true) + .handled(false) .beanRef('fconnectionService', 'handleDisconnection') .end() .setHeader(Fconnection.HEADER_FCONNECTION_ID, simple(ClickatellFconnection.this.id.toString())) @@ -39,7 +53,10 @@ class ClickatellFconnection extends Fconnection { 'user=${header.clickatell.username}&' + 'password=${header.clickatell.password}&' + 'to=${header.clickatell.dst}&' + - 'text=${body}')) + 'concat=${header.clickatell.concat}&' + + 'unicode=${header.clickatell.unicode}&' + + 'text=${body}' + + (ClickatellFconnection.this.sendToUsa ? '&mo=1&from=${header.clickatell.fromNumber}' : ''))) .to(CLICKATELL_URL) .process(new ClickatellPostProcessor()) .routeId("out-internet-${ClickatellFconnection.this.id}")] diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Contact.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Contact.groovy index e319f7994..cbf6c0433 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Contact.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Contact.groovy @@ -1,6 +1,8 @@ package frontlinesms2 class Contact { + def mobileNumberUtilService + //> PROPERTIES String name String mobile @@ -24,8 +26,7 @@ class Contact { static mapping = { sort name:'asc' - customFields cascade: 'all' - customFields sort: 'name','value' + customFields cascade:'all', sort:'name' } //> EVENT METHODS @@ -42,6 +43,10 @@ class Contact { GroupMembership.findAllByContact(this)*.group.sort{it.name} } + def getIsoCountryCode() { + mobile?getISOCountryCode(mobile):'' + } + def setGroups(groups) { this.groups.each() { GroupMembership.remove(this, it) } groups.each() { GroupMembership.create(this, it) } @@ -68,7 +73,7 @@ class Contact { } def getInboundMessagesCount() { - mobile ? Fmessage.countBySrcAndIsDeleted(mobile, false) : 0 + mobile ? TextMessage.countBySrcAndIsDeleted(mobile, false) : 0 } def getOutboundMessagesCount() { @@ -81,6 +86,9 @@ class Contact { mobile = n } + def getFlagCSSClasses() { + mobileNumberUtilService.getFlagCSSClasses(mobile) + } static findByCustomFields(fields) { def matches = [] diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/CustomActivity.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/CustomActivity.groovy new file mode 100644 index 000000000..d2b987a22 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/CustomActivity.groovy @@ -0,0 +1,39 @@ +package frontlinesms2 + +class CustomActivity extends Activity { + List steps + def customActivityService + static String getShortName() { 'customactivity' } + static hasMany = [steps: Step] + + static mapping = { + steps cascade: "all-delete-orphan" + } + + def getActivityMessages(getOnlyStarred=false, getSent=null, stepId=null, params=null) { + if(stepId) { + def outgoingMessagesByStep = [] + if((Step.get(stepId) instanceof ReplyActionStep) || (Step.get(stepId) instanceof ForwardActionStep)) { + outgoingMessagesByStep = MessageDetail.findAllByOwnerTypeAndOwnerId(MessageDetail.OwnerType.STEP, stepId).collect{ it.message } + } + return (outgoingMessagesByStep + TextMessage.owned(this, getOnlyStarred, true)?.list(params?:[:])).flatten() + } else { + TextMessage.owned(this, getOnlyStarred, getSent).list(params?:[:]) + } + } + + def processKeyword(TextMessage message, Keyword matchedKeyword) { + this.addToMessages(message) + this.save(flush:true) + customActivityService.triggerSteps(this, message) + } + + def activate() { + steps.each { it.activate() } + } + + def deactivate() { + steps.each { it.deactivate() } + } +} + diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Dispatch.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Dispatch.groovy index 6efdf3fba..044b0e5ee 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Dispatch.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Dispatch.groovy @@ -1,11 +1,12 @@ package frontlinesms2 class Dispatch { - static belongsTo = [message: Fmessage] + static belongsTo = [message: TextMessage] String dst DispatchStatus status Date dateSent def expressionProcessorService + Long fconnectionId boolean isDeleted @@ -15,6 +16,7 @@ class Dispatch { static constraints = { dst(nullable:false) + fconnectionId(nullable:true) status(nullable: false, validator: { val, obj -> if(val == DispatchStatus.SENT) obj.dateSent != null @@ -43,7 +45,6 @@ class Dispatch { def messageOwner = params.messageOwner def startDate = params.startDate.startOfDay def endDate = params.endDate.endOfDay - def statuses = params.messageStatus.collect { it.toLowerCase() } and { eq('isDeleted', false) diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Fconnection.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Fconnection.groovy index 8be53819d..6f0f7deb5 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Fconnection.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Fconnection.groovy @@ -4,52 +4,95 @@ import grails.util.Environment import org.apache.camel.builder.RouteBuilder import org.apache.camel.model.RouteDefinition +import org.apache.camel.Exchange +import org.codehaus.groovy.grails.commons.ApplicationHolder // Please don't instantiate this class. We would make it abstract if it didn't make testing // difficult, and stop us calling GORM queries across all subclasses. class Fconnection { + def fconnectionService + def dispatchRouterService + def mobileNumberUtilService + static final String HEADER_FCONNECTION_ID = 'fconnection-id' static transients = ['status', 'routeDefinitions'] static String getShortName() { 'base' } - - static hasMany = [messages: Fmessage] - - def fconnectionService - static def implementations = [SmslibFconnection, + static final implementations = [FrontlinesyncFconnection, + SmslibFconnection, ClickatellFconnection, IntelliSmsFconnection, + NexmoFconnection, + SmppFconnection, SmssyncFconnection] + static final getImplementations(params) { + (params.beta || Boolean.parseBoolean(ApplicationHolder.application.mainContext.getBean('appSettingsService').beta ?: 'false')) ? betaImplementations: implementations + } + + static getBetaImplementations() { implementations } static getNonnullableConfigFields = { clazz -> def fields = clazz.configFields - if(fields instanceof Map) return fields.getAllValues()?.findAll { field -> !clazz.constraints[field].blank } - else return fields.findAll { field -> - if(!(clazz.metaClass.hasProperty(null, field).type in [Boolean, boolean])) { - !clazz.constraints[field].nullable + if (fields) { + if(fields instanceof Map) return fields.getAllValues()?.findAll { field -> !clazz.constraints[field].blank } + else return fields.findAll { field -> + if(!(clazz.metaClass.hasProperty(null, field).type in [Boolean, boolean])) { + !clazz.constraints[field].nullable + } } + } else { + return fields } } static mapping = { sort id:'asc' tablePerHierarchy false + version false } - - String name - - static namedQueries = { - findByMessages { messageInstance -> - messages { - eq 'id', messageInstance.id - } - } + + static constraints = { + name blank:false } + String name + boolean sendEnabled = true + boolean receiveEnabled = true + boolean enabled = true + def getStatus() { fconnectionService.getConnectionStatus(this) } - + + def getMessages() { + TextMessage.findAllByConnectionId(this.id) + } + + def addToMessages(msg) { + msg.connectionId = this.id + msg.save() + } + + def getFlagCSSClasses() { + if('fromNumber' in this.properties) { + return mobileNumberUtilService.getFlagCSSClasses(fromNumber) + } else { + return 'flag' + } + } + + def updateDispatch(Exchange x) { + dispatchRouterService.updateDispatch(x, DispatchStatus.SENT) + } + + def getDisplayMetadata() { + return null + } + + boolean isUserMutable() { + return true + } + List getRouteDefinitions() { if(Environment.current != Environment.TEST) { throw new IllegalStateException("Do not know how to create routes for Fconnection of class: ${this.class}") diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Fmessage.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Fmessage.groovy deleted file mode 100644 index 8d1aa51d0..000000000 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Fmessage.groovy +++ /dev/null @@ -1,326 +0,0 @@ -package frontlinesms2 - -import groovy.time.* -import org.hibernate.criterion.CriteriaSpecification - -class Fmessage { - static final int MAX_TEXT_LENGTH = 1600 - - static belongsTo = [messageOwner:MessageOwner] - static transients = ['hasSent', 'hasPending', 'hasFailed', 'displayName' ,'outboundContactList', 'receivedOn'] - - Date date = new Date() // No need for dateReceived since this will be the same as date for received messages and the Dispatch will have a dateSent - Date dateCreated // This is unused and should be removed, but doing so throws an exception when running the app and I cannot determine why - - String src - String text - String inboundContactName - String outboundContactName - String ownerDetail - - boolean read - boolean starred - boolean archived - boolean isDeleted - - boolean inbound - - static hasMany = [dispatches:Dispatch] - - static mapping = { - sort date:'desc' - inboundContactName formula:'(SELECT c.name FROM contact c WHERE c.mobile=src)' - outboundContactName formula:'(SELECT MAX(c.name) FROM contact c, dispatch d WHERE c.mobile=d.dst AND d.message_id=id)' - version false - - } - - static constraints = { - messageOwner nullable:true - src(nullable:true, validator: { val, obj -> - val || !obj.inbound - }) - text maxSize:MAX_TEXT_LENGTH - inboundContactName nullable:true - outboundContactName nullable:true - archived(nullable:true, validator: { val, obj -> - obj.messageOwner == null || obj.messageOwner.archived == val - }) - inbound(nullable:true, validator: { val, obj -> - val ^ (obj.dispatches? true: false) - }) - dispatches nullable:true - ownerDetail nullable:true - } - - def beforeInsert = { - if(!this.inbound) this.read = true - } - - static namedQueries = { - inbox { getOnlyStarred=false, archived=false -> - and { - eq("isDeleted", false) - eq("archived", archived) - if(getOnlyStarred) - eq("starred", true) - eq("inbound", true) - isNull("messageOwner") - } - } - sent { getOnlyStarred=false, archived=false -> - and { - eq("isDeleted", false) - eq("archived", archived) - if(!archived) { - projections { dispatches { eq('status', DispatchStatus.SENT) } } - } else { - projections { - dispatches { - or { - eq('status', DispatchStatus.SENT) - eq('status', DispatchStatus.PENDING) - eq('status', DispatchStatus.FAILED) - } - } - } - } - if(getOnlyStarred) - eq("starred", true) - } - } - pending { getOnlyFailed=false -> - and { - eq("isDeleted", false) - eq("archived", false) - if(getOnlyFailed) { - projections { dispatches { eq('status', DispatchStatus.FAILED) } } - } else { - projections { - dispatches { - or { - eq('status', DispatchStatus.PENDING) - eq('status', DispatchStatus.FAILED) - } - } - } - } - } - projections { - distinct 'id' - property 'date' - property 'id' - } - } - deleted { getOnlyStarred=false -> - and { - eq("isDeleted", true) - eq("archived", false) - if(getOnlyStarred) - eq('starred', true) - } - } - owned { MessageOwner owner, boolean getOnlyStarred=false, getSent=null -> - and { - eq("isDeleted", false) - eq("messageOwner", owner) - if(getOnlyStarred) - eq("starred", true) - if(getSent != null) - eq("inbound", getSent) - } - } - unread { - and { - eq("isDeleted", false) - eq("archived", false) - eq("inbound", true) - eq("read", false) - isNull("messageOwner") - } - } - - search { search -> - def ids = Fmessage.withCriteria { - createAlias('dispatches', 'disp', CriteriaSpecification.LEFT_JOIN) - if(search.searchString) { - or { - ilike("text", "%${search.searchString}%") - ilike("src", "%${search.searchString}%") - ilike("disp.dst", "%${search.searchString}%") - } - } - if(search.contactString) { - def contactNumbers = Contact.findAllByNameIlike("%${search.contactString}%")*.mobile ?: [''] - or { - 'in'('src', contactNumbers) - 'in'('disp.dst', contactNumbers) - } - } - if(search.group) { - def groupMembersNumbers = search.group.addresses?: [''] //otherwise hibernate fail to search 'in' empty list - or { - 'in'('src', groupMembersNumbers) - 'in'('disp.dst', groupMembersNumbers) - } - } - if(search.status) { - if(search.status.toLowerCase() == 'inbound') eq('inbound', true) - else eq('inbound', false) - } - if(search.owners) { - 'in'("messageOwner", search.owners) - } - if(search.startDate && search.endDate) { - between("date", search.startDate, search.endDate) - } else if (search.startDate) { - ge("date", search.startDate) - } else if (search.endDate) { - le("date", search.endDate) - } - if(search.customFields.any { it.value }) { - // provide empty list otherwise hibernate fails to search 'in' empty list - def matchingContactsNumbers = Contact.findByCustomFields(search.customFields)*.mobile?: [''] - or { - 'in'("src", matchingContactsNumbers) - 'in'('disp.dst', matchingContactsNumbers) - } - } - if(!search.inArchive) { - eq('archived', false) - } - eq('isDeleted', false) - // order('date', 'desc') removed due to http://jira.grails.org/browse/GRAILS-8162; please reinstate when possible - }*.refresh()*.id // TODO this is ugly ugly, but it fixes issues with loading incomplete dispatches. Feel free to sort it out - 'in'('id', ids) - } - - forReceivedStats { params -> - def groupInstance = params.groupInstance - def messageOwner = params.messageOwner - def startDate = params.startDate.startOfDay - def endDate = params.endDate.endOfDay - - and { - eq('inbound', true) - eq('isDeleted', false) - between("date", startDate, endDate) - if(groupInstance) 'in'('src', groupInstance?.addresses ?: "") - if(messageOwner) 'in'('messageOwner', messageOwner) - } - } - } - - def getDisplayName() { - if(inbound) { - if(inboundContactName) return inboundContactName - else if(id) return src - else return Contact.findByMobile(src)?.name?: src - } else if(dispatches.size() == 1) { - if(outboundContactName) return outboundContactName - else { - def dst = (dispatches as List)[0].dst - if(id) return dst - else return Contact.findByMobile(dst)?.name?: dst - } - } else { - return Integer.toString(dispatches.size()) - } - } - - def getHasSent() { areAnyDispatches(DispatchStatus.SENT) } - def getHasFailed() { areAnyDispatches(DispatchStatus.FAILED) } - def getHasPending() { areAnyDispatches(DispatchStatus.PENDING) } - def getOutboundContactList(){ - def contactlist = [] - dispatches.each{ contactlist << Contact.findByMobile(it.dst)?.name } - contactlist?contactlist:"" - } - - private boolean isMoveAllowed(){ - if(this.messageOwner){ - return !(this.messageOwner?.archived) - } else { - return (!this.isDeleted && !this.archived) - } - } - - private def areAnyDispatches(status) { - dispatches?.any { it.status == status } - } - - public void setText(String text) { - this.text = text?.truncate(MAX_TEXT_LENGTH) - } - - static def listPending(onlyFailed, params=[:]) { - Fmessage.getAll(pending(onlyFailed).list(params) as List) - } - - static def countPending(onlyFailed) { - pending(onlyFailed).list().size() - } - - static def hasFailedMessages() { - return pending(true).count() > 0 - } - - static def countUnreadMessages() { - Fmessage.unread.count() - } - - static def countAllMessages(params) { - def inboxCount = Fmessage.inbox.count() - def sentCount = Fmessage.sent.count() - def pendingCount = Fmessage.pending.count() - def deletedCount = Fmessage.deleted.count() - [inbox: inboxCount, sent: sentCount, pending: pendingCount, deleted: deletedCount] - } - - // TODO should this be in a service? - static def getMessageStats(params) { - def asKey = { date -> date.format('dd/MM') } - - def dates = [:] - (params.startDate..params.endDate).each { date -> - dates[asKey(date)] = [sent:0, received:0] - } - - if(!params.inbound) { - // TODO the named query should ideally do the counts for us - Dispatch.forSentStats(params).list().each { d -> - ++dates[asKey(d.dateSent)].sent - } - } - - if(params.inbound == null || params.inbound) { - // TODO the named query should ideally do the counts for us - Fmessage.forReceivedStats(params).list().each { m -> - ++dates[asKey(m.date)].received - } - } - - dates - } - - //TODO: Remove in Groovy 1.8 (Needed for 1.7) - private static def countAnswer(final Map answer, Object mappedKey) { - if (!answer.containsKey(mappedKey)) { - answer.put(mappedKey, 0) - } - int current = answer.get(mappedKey) - answer.put(mappedKey, current + 1) - } - - def updateDispatches() { - if(isDeleted) { - dispatches.each { - it.isDeleted = true - } - } - } - - def getReceivedOn() { - Fconnection.findByMessages(this).list()[0] - } -} diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Folder.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Folder.groovy index 4a89a6f48..1dcdaf76c 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Folder.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Folder.groovy @@ -25,11 +25,11 @@ class Folder extends MessageOwner { //> ACCESSORS def getFolderMessages(getOnlyStarred=false, getSent=null) { - Fmessage.owned(this, getOnlyStarred, getSent) + TextMessage.owned(this, getOnlyStarred, getSent) } def getLiveMessageCount() { - def m = Fmessage.findAllByMessageOwnerAndIsDeleted(this, false) + def m = TextMessage.findAllByMessageOwnerAndIsDeleted(this, false) m ? m.size() : 0 } @@ -44,7 +44,7 @@ class Folder extends MessageOwner { def unarchive() { this.archived = false - def messagesToArchive = Fmessage?.owned(this, false, true)?.list() + def messagesToArchive = TextMessage?.owned(this, false, true)?.list() messagesToArchive.each { it?.archived = false } } diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/ForwardActionStep.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/ForwardActionStep.groovy new file mode 100644 index 000000000..9a3a6fe27 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/ForwardActionStep.groovy @@ -0,0 +1,86 @@ +package frontlinesms2 + +// TODO please clean up this code's formatting +class ForwardActionStep extends Step { + def grailsApplication + def autoforwardService + + static service = 'autoforward' + static action = 'doForward' + static String getShortName() { 'forward' } + + static configFields = [sentMessageText: 'textarea', recipients: ''] + + Map getConfig() { + def config = [stepId:id, sentMessageText:sentMessageText] + ['contacts':'Contact', 'groups':'Group', 'smartGroups':'SmartGroup', 'addresses':'Address'].each { name, type -> + config."$name" = getRecipientsByDomain(type) + } + config + } + + def getSentMessageText() { + getPropertyValue("sentMessageText") + } + + def getRecipients() { + def addresses = getRecipientsByDomain("Address") + def groups = getRecipientsByDomain("Group") + def smartGroups = getRecipientsByDomain("SmartGroup") + def contacts = getRecipientsByDomain("Contact") + + groups.each { group-> addresses << group.members.collect{ it.mobile } } + smartGroups.each { group-> addresses << group.members.collect{ it.mobile } } + addresses << contacts.collect{ it.mobile } + + return addresses.flatten().unique() + } + + def getRecipientsByDomain(domainName) { + def addresses = [] + if(domainName == "Address") { + stepProperties.collect { step-> + if(step.value.startsWith(domainName)) { + addresses << step.value.substring(domainName.size() + 1, step.value.size()) + } + } + return addresses - null + } + + // FIXME WTF is this doing? Is this a dumb way of doing Class.forName(), or just passing the class to the method in the first place? + def domain = grailsApplication.domainClasses*.clazz.find { (it.name - "frontlinesms2.") == domainName } + def domainInstances = stepProperties.collect { step-> + if(step.value.startsWith(domainName)) { + domain.get(step.value.substring(domainName.size() + 1, step.value.size()) as Long) + } + } + domainInstances - null + } + + def setRecipients(contacts, groups, smartGroups, addresses) { + stepProperties.findAll { it.key == "recipient" }.each { removeFromStepProperties(it) } + if(groups) + setRecipientsBydomain("Group", groups) + if(smartGroups) + setRecipientsBydomain("SmartGroup", smartGroups) + if(contacts) + setRecipientsBydomain("Contact", contacts) + if(addresses) + setRecipientsBydomain("Address", addresses) + } + + private def setRecipientsBydomain(domainName, instanceList) { + instanceList.each { + this.addToStepProperties(new StepProperty(key:"recipient", value:"${domainName}-${domainName == 'Address' ? it : it.id}")).save() + } + } + + def process(TextMessage message) { + autoforwardService.doForward(this, message) + } + + def getDescription() { + i18nUtilService.getMessage(code:"customactivity.${this.shortName}.description", args:[this.sentMessageText]) + } + +} diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/FrontlinesyncFconnection.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/FrontlinesyncFconnection.groovy new file mode 100644 index 000000000..383afe515 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/FrontlinesyncFconnection.groovy @@ -0,0 +1,91 @@ +package frontlinesms2 + +import org.apache.camel.builder.RouteBuilder +import org.apache.camel.model.RouteDefinition +import org.apache.camel.Exchange + +import frontlinesms2.api.* + +@FrontlineApiAnnotations(apiUrl="frontlinesync") +class FrontlinesyncFconnection extends Fconnection implements FrontlineApi { + static final checkIntervalOptions = [1, 5, 15, 30, 60, 120, 0] + static String getShortName() { 'frontlinesync' } + static final passwords = [] + static final configFields = ['info-setup': ['secret'], 'info-name':['name']] + + def frontlinesyncService + def appSettingsService + def grailsLinkGenerator + def urlHelperService + def dispatchRouterService + + Date lastConnectionTime + boolean sendEnabled = false + boolean receiveEnabled = false + boolean missedCallEnabled = false + boolean configSynced = false + boolean hasDispatches = false + int checkInterval = 15 + String secret + + static constraints = { + lastConnectionTime nullable:true + } + + def apiProcess(controller) { + frontlinesyncService.apiProcess(this, controller) + } + + boolean isApiEnabled() { return this.sendEnabled || this.receiveEnabled } + + def getCustomStatus() { + lastConnectionTime ? (this.enabled ? ConnectionStatus.CONNECTED : ConnectionStatus.DISABLED) : ConnectionStatus.CONNECTING + } + + List getRouteDefinitions() { + def routeDefinitions = new RouteBuilder() { + @Override void configure() {} + List getRouteDefinitions() { + def definitions = [] + if(isSendEnabled()) { + definitions << from("seda:out-${FrontlinesyncFconnection.this.id}") + .setHeader('fconnection-id', simple(FrontlinesyncFconnection.this.id.toString())) + .beanRef('frontlinesyncService', 'processSend') + .routeId("out-internet-${FrontlinesyncFconnection.this.id}") + } + return definitions + } + }.routeDefinitions + return routeDefinitions + } + + def getIndexOfCurrentCheckFrequency() { + return checkIntervalOptions.indexOf(checkInterval) + } + + String getFullApiUrl(request) { + return apiEnabled? "${urlHelperService.getBaseUrl(request)}" :'' + } + + def removeDispatchesFromQueue() { + QueuedDispatch.deleteAll(this) + if(this.hasDispatches) { + this.hasDispatches = false + this.save() + } + } + + def addToQueuedDispatches(d) { + QueuedDispatch.create(this, d) + this.hasDispatches = true + } + + def getQueuedDispatches() { + QueuedDispatch.getDispatches(this) + } + + def updateDispatch(Exchange x) { + // Dispatch is already in PENDING state so no need to change the status + } +} + diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Group.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Group.groovy index f5c717532..b40ab532d 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Group.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Group.groovy @@ -19,15 +19,15 @@ class Group { sort name:'asc' } - def beforeDelete = { - GroupMembership.deleteFor(this) - } - def getMembers() { // TODO shouldn't have to filter the GroupMemberships manually here Contact.findAll("FROM Contact c, GroupMembership m WHERE m.group=? AND m.contact=c ORDER BY c.name", [this]).collect{ it[0] } } + def countMembers() { + Group.executeQuery("SELECT COUNT(*) From GroupMembership m WHERE m.group=?", [this])[0] + } + def addToMembers(Contact c) { GroupMembership.create(c, this) } diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/GroupMembership.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/GroupMembership.groovy index aee7c1c5a..6e8065e4e 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/GroupMembership.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/GroupMembership.groovy @@ -35,7 +35,7 @@ class GroupMembership implements Serializable { return contactGroup ? contactGroup.delete(flush: flush) : false } - static void deleteFor(Contact c, boolean flush=false) { + static void deleteFor(Contact c) { executeUpdate("DELETE FROM GroupMembership WHERE contact=:contact", [contact: c]) } diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/IntelliSmsFconnection.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/IntelliSmsFconnection.groovy index f6cb1e762..92d4a5a92 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/IntelliSmsFconnection.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/IntelliSmsFconnection.groovy @@ -10,18 +10,16 @@ import frontlinesms2.camel.exception.* class IntelliSmsFconnection extends Fconnection { private static final String INTELLISMS_URL = 'http://www.intellisoftware.co.uk/smsgateway/sendmsg.aspx?' static configFields = [name:null, - send: ['username', 'password'], - receive: ['receiveProtocol', 'serverName', 'serverPort', 'emailUserName', 'emailPassword']] - static passwords = ['password', 'emailPassword'] + sendEnabled: ['username', 'password'], + receiveEnabled: ['receiveProtocol', 'serverName', 'serverPort', 'emailUserName', 'emailPassword']] + static passwords = [] static defaultValues = [] static String getShortName() { 'intellisms' } String username String password // FIXME maybe encode this rather than storing plaintext - // TODO rename sendEnabled - boolean send - // TODO rename receiveEnabled - boolean receive + boolean sendEnabled + boolean receiveEnabled //Http forwarding configuration EmailReceiveProtocol receiveProtocol @@ -35,15 +33,15 @@ class IntelliSmsFconnection extends Fconnection { } static constraints = { - send(validator: { val, obj -> + sendEnabled(validator: { val, obj -> if(val) { return obj.username && obj.password } - else return obj.receive + else return obj.receiveEnabled }) - receive(validator: { val, obj -> + receiveEnabled(validator: { val, obj -> if(val) return obj.receiveProtocol && obj.serverName && obj.emailUserName && obj.emailUserName - else return obj.send + else return obj.sendEnabled }) username(nullable:true, blank:false) password(nullable:true, blank:false) @@ -66,10 +64,10 @@ class IntelliSmsFconnection extends Fconnection { @Override void configure() {} List getRouteDefinitions() { def definitions = [] - if(isSend()) { + if(isSendEnabled()) { definitions << from("seda:out-${IntelliSmsFconnection.this.id}") .onException(AuthenticationException) - .handled(true) + .handled(false) .beanRef('fconnectionService', 'handleDisconnection') .end() .setHeader(Fconnection.HEADER_FCONNECTION_ID, simple(IntelliSmsFconnection.this.id.toString())) @@ -83,7 +81,7 @@ class IntelliSmsFconnection extends Fconnection { .process(new IntelliSmsPostProcessor()) .routeId("out-internet-${IntelliSmsFconnection.this.id}") } - if(isReceive()) { + if(isReceiveEnabled()) { definitions << from(camelProducerAddress()) .setHeader(Fconnection.HEADER_FCONNECTION_ID, simple(IntelliSmsFconnection.this.id.toString())) .beanRef('intelliSmsTranslationService', 'process') diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Interaction.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Interaction.groovy new file mode 100644 index 000000000..21f84146e --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Interaction.groovy @@ -0,0 +1,135 @@ +package frontlinesms2 + +import groovy.time.* + +class Interaction { + + static belongsTo = [messageOwner:MessageOwner] + static transients = ['hasSent', 'hasPending', 'hasFailed', 'displayName' ,'outboundContactList', 'read', 'receivedOn'] + + Date date = new Date() // No need for dateReceived since this will be the same as date for received messages and the Dispatch will have a dateSent + Date dateCreated // This is unused and should be removed, but doing so throws an exception when running the app and I cannot determine why + + String src + String outboundContactName + String inboundContactName + Long connectionId + boolean rd + boolean starred + boolean archived + boolean isDeleted + boolean inbound + + static mapping = { + table 'fmessage' + tablePerHierarchy true + sort date:'desc' + inboundContactName formula:'(SELECT c.name FROM contact c WHERE c.mobile=src)' + outboundContactName formula:'(SELECT MAX(c.name) FROM contact c, dispatch d WHERE c.mobile=d.dst AND d.message_id=id)' + version false + } + + static constraints = { + messageOwner nullable:true + src(nullable:true, validator: { val, obj -> + val || !obj.inbound + }) + inboundContactName nullable:true + outboundContactName nullable:true + archived(nullable:true, validator: { val, obj -> + obj.messageOwner == null || obj.messageOwner.archived == val + }) + connectionId nullable:true + } + + def beforeInsert = { + if(!this.inbound) this.read = true + } + + def getReceivedOn() { + Fconnection.get(this.connectionId) + } + + static namedQueries = { + deleted { getOnlyStarred=false -> + and { + eq("isDeleted", true) + eq("archived", false) + if(getOnlyStarred) + eq('starred', true) + } + } + owned { MessageOwner owner, boolean getOnlyStarred=false, getSent=null -> + and { + eq("isDeleted", false) + eq("messageOwner", owner) + if(getOnlyStarred) + eq("starred", true) + if(getSent != null) + eq("inbound", getSent) + } + } + unread { MessageOwner owner=null -> + and { + eq("isDeleted", false) + eq("archived", false) + eq('rd', false) + if(owner == null) + isNull("messageOwner") + else + eq("messageOwner", owner) + } + } + totalUnread { + and { + eq("isDeleted", false) + eq("archived", false) + eq('rd', false) + } + } + search { ids -> + 'in'('id', ids) + } + } + + def getDisplayName() { + return "Override me" + } + + private boolean isMoveAllowed() { + if(this.messageOwner){ + return !(this.messageOwner?.archived) + } else { + return (!this.isDeleted && !this.archived) + } + } + + public boolean isRead() { return this.rd } + public boolean setRead(boolean read) { this.rd = read } + + static def countUnreadMessages() { + Interaction.unread.count() + } + + static def countUnreadMessages(owner) { + Interaction.unread(owner).count() + } + + static def countTotalUnreadMessages() { + Interaction.totalUnread.count() + } + + //TODO: Remove in Groovy 1.8 (Needed for 1.7) + private static def countAnswer(final Map answer, Object mappedKey) { + if (!answer.containsKey(mappedKey)) { + answer.put(mappedKey, 0) + } + int current = answer.get(mappedKey) + answer.put(mappedKey, current + 1) + } + + private def getOwnerType(owner) { + owner instanceof Step ? MessageDetail.OwnerType.STEP : MessageDetail.OwnerType.ACTIVITY + } +} + diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/JoinActionStep.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/JoinActionStep.groovy new file mode 100644 index 000000000..05cab91fb --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/JoinActionStep.groovy @@ -0,0 +1,39 @@ +package frontlinesms2 + +class JoinActionStep extends Step { + def subscriptionService + + static service = 'subscription' + static action = 'doJoin' + static String getShortName() { 'join' } + + static configFields = [group: Group] + + static constraints = { + } + + Map getConfig() { + [stepId:id, groupId:getGroupId()] + } + + def getGroup() { + Group.get(getGroupId()) + } + + def getGroupId() { + getPropertyValue("group") + } + + def setGroup(Group group) { + setPropertyValue("group", group.id) + } + + def process(TextMessage message) { + subscriptionService.doJoin(this, message) + } + + def getDescription() { + i18nUtilService.getMessage(code:"customactivity.${this.shortName}.description", args:[this?.group?.name]) + } +} + diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/LeaveActionStep.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/LeaveActionStep.groovy new file mode 100644 index 000000000..87852fc54 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/LeaveActionStep.groovy @@ -0,0 +1,37 @@ +package frontlinesms2 + +class LeaveActionStep extends Step { + def subscriptionService + static service = 'subscription' + static action = 'doLeave' + static String getShortName() { 'leave' } + + static configFields = [group: Group] + + static constraints = { + } + + Map getConfig() { + [stepId:id, groupId:getGroupId()] + } + + def getGroup() { + Group.get(getPropertyValue("group")) + } + + def getGroupId() { + getPropertyValue("group") + } + + def setGroup(Group group) { + setPropertyValue("group", group.id) + } + + def process(TextMessage message) { + subscriptionService.doLeave(this, message) + } + + def getDescription() { + i18nUtilService.getMessage(code:"customactivity.${this.shortName}.description", args:[this?.group?.name]) + } +} diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/LogEntry.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/LogEntry.groovy index fa6a378ad..74bbe64ab 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/LogEntry.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/LogEntry.groovy @@ -10,7 +10,7 @@ class LogEntry { } static def log(content) { - new LogEntry(date: new Date(), content: content).save() + new LogEntry(date: new Date(), content: content).save(failOnError:true) return content } } diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/MessageDetail.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/MessageDetail.groovy new file mode 100644 index 000000000..d7c080741 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/MessageDetail.groovy @@ -0,0 +1,14 @@ +package frontlinesms2 + +class MessageDetail { + static belongsTo = [message: TextMessage] + enum OwnerType { ACTIVITY, STEP } + + OwnerType ownerType + Long ownerId + String value + + static constraints = { + } + +} diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/MessageOwner.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/MessageOwner.groovy index be0a786b3..b36df5de1 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/MessageOwner.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/MessageOwner.groovy @@ -2,7 +2,7 @@ package frontlinesms2 abstract class MessageOwner { String name - static hasMany = [messages: Fmessage] + static hasMany = [messages: TextMessage] boolean archived boolean deleted @@ -12,7 +12,10 @@ abstract class MessageOwner { version false } - def getDisplayText(Fmessage msg) { + def getDisplayText(TextMessage msg) { msg.text } + + def getMoreActions() { [] } } + diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/MissedCall.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/MissedCall.groovy new file mode 100644 index 000000000..50c8cae07 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/MissedCall.groovy @@ -0,0 +1,68 @@ +package frontlinesms2 + +import groovy.time.* + +class MissedCall extends Interaction { + def mobileNumberUtilService + + boolean inbound = true + String outboundContactName = null + + static constraints = { + inboundContactName nullable:true + outboundContactName nullable:true + } + + static namedQueries = { + inbox { getOnlyStarred=false, archived=false -> + and { + eq("isDeleted", false) + eq("archived", archived) + if(getOnlyStarred) + eq("starred", true) + } + } + forReceivedStats { params -> + def startDate = params.startDate.startOfDay + def endDate = params.endDate.endOfDay + and { + eq('isDeleted', false) + between("date", startDate, endDate) + } + } + } << Interaction.namedQueries // Named Queries are not inherited + + def getDisplayName() { + if(inboundContactName) return inboundContactName + else if(id) return src.toPrettyPhoneNumber() + else return Contact.findByMobile(src)?.name?: src.toPrettyPhoneNumber() + } + + def getContactFlagCSSClasses() { + def flagCssClass + mobileNumberUtilService.getFlagCSSClasses(src) + } + + static def countAllMissedCalls() { + ['inbox', 'deleted'].collectEntries { [it, MissedCall[it].count()] } + } + + static def countUnread() { + MissedCall.unread.count() + } + + static def getMessageStats(params) { + def asKey = { date -> date.format('dd/MM') } + + def dates = [:] + (params.startDate..params.endDate).each { date -> + dates[asKey(date)] = [sent:0, received:0] + } + MissedCall.forReceivedStats(params).list().each { m -> + ++dates[asKey(m.date)].received + } + + dates + } +} + diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/NexmoFconnection.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/NexmoFconnection.groovy new file mode 100644 index 000000000..f2eaa10d7 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/NexmoFconnection.groovy @@ -0,0 +1,73 @@ +package frontlinesms2 + +import frontlinesms2.camel.nexmo.* + +import org.apache.camel.Exchange +import org.apache.camel.builder.RouteBuilder +import org.apache.camel.model.RouteDefinition +import frontlinesms2.camel.exception.* +import frontlinesms2.api.* + +@FrontlineApiAnnotations(apiUrl="nexmo") +class NexmoFconnection extends Fconnection implements FrontlineApi { + private static final String NEXMO_URL = 'http://rest.nexmo.com/sms/json?' + static final configFields = [ name:null, api_key:null, api_secret:null, fromNumber:null, receiveEnabled:null, sendEnabled:null ] + static final defaultValues = [ receiveEnabled:true, sendEnabled:true ] + static String getShortName() { 'nexmo' } + + String api_key + String api_secret + String fromNumber + boolean receiveEnabled = true + boolean sendEnabled = true + + static passwords = [] + + static constraints = { + api_key blank:false + api_secret blank:false + fromNumber blank:false + } + + def nexmoService + def urlHelperService + def grailsLinkGenerator + + def apiProcess(controller) { + nexmoService.apiProcess(this, controller) + } + + String getSecret(){ return "" } // No Secret for Nexmo + + boolean isApiEnabled() { this.receiveEnabled } + + String getFullApiUrl(request) { + def entityClassApiUrl = NexmoFconnection.getAnnotation(FrontlineApiAnnotations.class)?.apiUrl() + def path = grailsLinkGenerator.link(controller: 'api', params:[entityClassApiUrl: entityClassApiUrl, entityId: id], absolute: false) + return apiEnabled? "${urlHelperService.getBaseUrl(request)}$path" : '' + } + + List getRouteDefinitions() { + return new RouteBuilder() { + @Override void configure() {} + List getRouteDefinitions() { + return [from("seda:out-${NexmoFconnection.this.id}") + .onException(AuthenticationException, InvalidApiIdException, InsufficientCreditException) + .handled(false) + .beanRef('fconnectionService', 'handleDisconnection') + .end() + .setHeader(Fconnection.HEADER_FCONNECTION_ID, simple(NexmoFconnection.this.id.toString())) + .process(new NexmoPreProcessor()) + .setHeader(Exchange.HTTP_QUERY, + simple('api_key=${header.nexmo.api_key}&' + + 'api_secret=${header.nexmo.api_secret}&' + + 'from=${header.nexmo.fromNumber}&' + + 'to=${header.nexmo.dst}&' + + 'text=${body}')) + .to(NEXMO_URL) + .process(new NexmoPostProcessor()) + .routeId("out-internet-${NexmoFconnection.this.id}")] + } + }.routeDefinitions + } +} diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Poll.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Poll.groovy index 66f9c3941..f99824266 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Poll.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Poll.groovy @@ -8,13 +8,14 @@ class Poll extends Activity { //> SERVICES def messageSendService + def pollService //> PROPERTIES String autoreplyText String question boolean yesNo - List responses static hasMany = [responses: PollResponse] + SortedSet responses //> SETTINGS static transients = ['unknown'] @@ -24,16 +25,9 @@ class Poll extends Activity { version false } - def getDisplayText(Fmessage msg) { - def p = PollResponse.withCriteria { - messages { - eq('isDeleted', false) - eq('archived', false) - eq('id', msg.id) - } - } - - p?.size() ? "${p[0].value} (\"${msg.text}\")" : msg.text + def getDisplayText(TextMessage msg) { + def p = responses.find { "$it.id" == msg.ownerDetail } + p? "${p.value} (\"${msg.text}\")": msg.text } static constraints = { @@ -57,20 +51,21 @@ class Poll extends Activity { def getUnknown() { responses.find { it.key == KEY_UNKNOWN } } - Poll addToMessages(Fmessage message) { + Poll addToMessages(TextMessage message) { if(!messages) messages = [] messages << message message.messageOwner = this + this.save() if(message.inbound) { this.responses.each { it.removeFromMessages(message) } - this.unknown.messages.add(message) + this.unknown.addToMessages(message) } this } - Poll removeFromMessages(Fmessage message) { + Poll removeFromMessages(TextMessage message) { this.messages?.remove(message) if(message.inbound) { this.responses.each { @@ -82,8 +77,8 @@ class Poll extends Activity { } def getResponseStats() { - def totalMessageCount = getActivityMessages().count() - responses.sort {it.key?.toLowerCase()}.collect { + def totalMessageCount = messages?.count { it.inbound && !it.isDeleted && (it.archived == this.archived) }?: 0 + responses.collect { def messageCount = it.liveMessageCount [id: it.id, value: it.value, @@ -121,8 +116,11 @@ class Poll extends Activity { this.keywords?.clear() def keys = attrs.findAll { it ==~ /keywords[A-E]=.*/ } println "###### Keywords :: ${keys}" - attrs.topLevelKeyword?.replaceAll(/\s/, "").split(",").each{ - this.addToKeywords(new Keyword(value:"${it.trim().toUpperCase()}")) + if(attrs.topLevelKeyword?.trim().length() > 0) { + attrs.topLevelKeyword?.replaceAll(/\s/, "").trim().split(",").each{ + println "## Setting Poll topLevelKeyword # $it" + this.addToKeywords(new Keyword(value:"${it.trim().toUpperCase()}")) + } } println "Keywords after setting Most TopLevel ## ${this.keywords*.value}" keys.each { k, v -> @@ -132,7 +130,8 @@ class Poll extends Activity { println "###### V :: ${v}" println attrs["keywords${k}"] attrs["keywords${k}"]?.replaceAll(/\s/, "").split(",").each{ - this.addToKeywords(new Keyword(value:"${it.toUpperCase()}", ownerDetail:"${k}", isTopLevel:!attrs.topLevelKeyword?.trim()))//adds the keyword without setting the ownerDetail as pollResponse.id + if(it.size() > 0) + this.addToKeywords(new Keyword(value:"${it.toUpperCase()}", ownerDetail:"${k}", isTopLevel:!(attrs.topLevelKeyword?.trim().length() > 0)))//adds the keyword without setting the ownerDetail as pollResponse.id } } } @@ -146,37 +145,32 @@ class Poll extends Activity { if(raw) raw.toUpperCase().replaceAll(/\s/, "").split(",").findAll { it }.join(", ") } - def deleteResponse(PollResponse response) { - response.messages.findAll { message -> - this.unknown.messages.add(message) - } - this.removeFromResponses(response) - response.delete() - this - } - - def processKeyword(Fmessage message, Keyword keyword) { + def processKeyword(TextMessage message, Keyword keyword) { def response = getPollResponse(message, keyword) - response.addToMessages(message) - response.save() - def poll = this - if(poll.autoreplyText) { - def params = [:] - params.addresses = message.src - params.messageText = poll.autoreplyText - def outgoingMessage = messageSendService.createOutgoingMessage(params) - poll.addToMessages(outgoingMessage) - messageSendService.send(outgoingMessage) - poll.save(failOnError:true) + response?.addToMessages(message) + response?.save() + if(this.autoreplyText) { + pollService.sendPollReply(this, message) } + this.save(failOnError:true) } - def PollResponse getPollResponse(Fmessage message, Keyword keyword) { - if(keyword.isTopLevel && !keyword.ownerDetail){ + def PollResponse getPollResponse(TextMessage message, Keyword keyword) { + if(!keyword || (keyword?.isTopLevel && !keyword?.ownerDetail)){ return this.unknown } else { - return this.responses.find{ keyword.ownerDetail == it.key } + return this.responses.find { keyword.ownerDetail == it.key } + } + } + + def deleteResponse(PollResponse response) { + response.messages.findAll { message -> + this.unknown.addToMessages(message) } + this.removeFromResponses(response) + response.delete() + this } + } diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/PollResponse.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/PollResponse.groovy index eb5077911..e9063eaae 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/PollResponse.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/PollResponse.groovy @@ -1,12 +1,9 @@ package frontlinesms2 -class PollResponse { +class PollResponse implements Comparable { String key String value - boolean isUnknownResponse = false static belongsTo = [poll: Poll] - static hasMany = [messages: Fmessage] - List messages = [] static transients = ['liveMessageCount'] static mapping = { @@ -14,33 +11,47 @@ class PollResponse { } static constraints = { - value(blank:false, nullable:false, maxSize:255) - poll(nullable:false) - messages(nullable:true) - key(nullable:true) + value(blank:false, maxSize:255) + } + + int compareTo(that) { + key.compareTo(that.key) + } + + def removeFromMessages(m) { + if(m.ownerDetail == "$id") m.setMessageDetail(this.poll, "") + } + + boolean isUnknown() { + return key == Poll.KEY_UNKNOWN } - void addToMessages(Fmessage message) { - if(message.inbound) { - this.poll.responses?.each { - it.removeFromMessages(message) - } - this.messages.add(message) - if (this.poll.messages == null) - this.poll.messages = [] - this.poll.messages << message - message.messageOwner = this.poll - message.save() + List getMessages() { + if(poll.messages == null) return [] + if(isUnknown()) { + return poll.messages.findAll { it.ownerDetail == Poll.KEY_UNKNOWN && it.inbound }.asList() } + return poll.messages.findAll { it.ownerDetail == "$id" && it.inbound }.asList() + } + + void addToMessages(TextMessage message) { + if(!message.inbound) return + if (this.poll.messages == null) + this.poll.messages = [] + this.poll.messages << message + message.messageOwner = this.poll + // TODO do we really need different handling for the unknown repsonse here? + if(isUnknown()) { + message.setMessageDetail(this.poll, Poll.KEY_UNKNOWN) + } else { + if(!id) throw new IllegalStateException('Cannot add a message to an unsaved PollResponse.') + message.setMessageDetail(this.poll, "$id") + } + message.save() } def getLiveMessageCount() { - def m = 0 - this.messages.each { - if(!it.isDeleted) - m++ - } - m + messages.count { it.inbound && !it.isDeleted && (it.archived == poll.archived) } } //> FACTORY METHODS @@ -48,3 +59,4 @@ class PollResponse { new PollResponse(value:'Unknown', key:Poll.KEY_UNKNOWN) } } + diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SmssyncFconnectionQueuedDispatch.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/QueuedDispatch.groovy similarity index 51% rename from plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SmssyncFconnectionQueuedDispatch.groovy rename to plugins/frontlinesms-core/grails-app/domain/frontlinesms2/QueuedDispatch.groovy index 74bd62766..cbef8985f 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SmssyncFconnectionQueuedDispatch.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/QueuedDispatch.groovy @@ -2,18 +2,17 @@ package frontlinesms2 import org.apache.commons.lang.builder.HashCodeBuilder -class SmssyncFconnectionQueuedDispatch implements Serializable { +class QueuedDispatch implements Serializable { static mapping = { id composite: ['connectionId', 'dispatchId'] version false - table 'smssync_dispatch' } long connectionId long dispatchId boolean equals(that) { - that instanceof SmssyncFconnectionQueuedDispatch && + that instanceof QueuedDispatch && that.connectionId == this.connectionId && that.dispatchId == this.dispatchId } @@ -22,20 +21,26 @@ class SmssyncFconnectionQueuedDispatch implements Serializable { return new HashCodeBuilder().append(connectionId).append(dispatchId).toHashCode() } - static SmssyncFconnectionQueuedDispatch create(SmssyncFconnection connection, Dispatch dispatch, boolean flush=false) { - new SmssyncFconnectionQueuedDispatch(connectionId:connection.id, + static QueuedDispatch create(Fconnection connection, Dispatch dispatch, boolean flush=false) { + new QueuedDispatch(connectionId:connection.id, dispatchId:dispatch.id) .save(flush:flush, insert:true) } - static void delete(SmssyncFconnection c, dispatches) { - executeUpdate "DELETE FROM SmssyncFconnectionQueuedDispatch WHERE connectionId=:connectionId AND dispatchId in :dispatchIds", - [connectionId:c.id, dispatchIds:dispatches*.id] + static void delete(Fconnection c, dispatches) { + if(dispatches) { + executeUpdate "DELETE FROM QueuedDispatch WHERE connectionId=:connectionId AND dispatchId in :dispatchIds", + [connectionId:c.id, dispatchIds:dispatches*.id] + } + } + + static void deleteAll(Fconnection c) { + executeUpdate ("DELETE FROM QueuedDispatch WHERE connectionId=:connectionId", [connectionId:c.id]) } static getDispatches(connection) { // TODO should do this in a single query - def dispatchIds = Dispatch.executeQuery("SELECT q.dispatchId FROM SmssyncFconnectionQueuedDispatch q WHERE q.connectionId=:connectionId", + def dispatchIds = Dispatch.executeQuery("SELECT q.dispatchId FROM QueuedDispatch q WHERE q.connectionId=:connectionId", [connectionId:connection.id]) Dispatch.getAll(dispatchIds) - null } diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/ReplyActionStep.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/ReplyActionStep.groovy new file mode 100644 index 000000000..f3265a4cd --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/ReplyActionStep.groovy @@ -0,0 +1,34 @@ +package frontlinesms2 + +class ReplyActionStep extends Step { + def autoreplyService + static service = 'autoreply' + static action = 'doReply' + static String getShortName() { 'reply' } + + static configFields = [autoreplyText: 'textarea'] + + static constraints = { + } + + Map getConfig() { + [stepId:id, autoreplyText:autoreplyText] + } + + def getAutoreplyText() { + getPropertyValue("autoreplyText") + } + + def getAutoreplyTextSummary() { + this.autoreplyText.truncate(20); + } + + def process(TextMessage message) { + autoreplyService.doReply(this, message) + } + + def getDescription() { + i18nUtilService.getMessage(code:"customactivity.${this.shortName}.description", args:[this.autoreplyTextSummary]) + } + +} diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/RequestParameter.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/RequestParameter.groovy index cba5b7acc..55119f8d9 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/RequestParameter.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/RequestParameter.groovy @@ -9,11 +9,9 @@ class RequestParameter { value(blank:false) } - static def regex = /[$][{]*[a-z_]*[}]/ - static belongsTo = [connection:Webconnection] - String getProcessedValue(Fmessage msg) { + String getProcessedValue(TextMessage msg) { def val = this.value def matches = val.findAll(regex) matches.each { match -> @@ -22,10 +20,12 @@ class RequestParameter { return val } - String getReplacement(String arg, Fmessage msg) { + String getReplacement(String arg, TextMessage msg) { arg = (arg - '${') - '}' def c = Webconnection.subFields[arg] - return c(msg) + if (c) + return c(msg) + else + return arg } - } \ No newline at end of file diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Search.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Search.groovy index 3aca89199..ce7b20874 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Search.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Search.groovy @@ -14,6 +14,7 @@ class Search { Date endDate Map customFields boolean inArchive + boolean starredOnly static constraints = { name(blank: false, nullable: false, maxSize: 255) @@ -27,5 +28,6 @@ class Search { endDate(nullable: true) customFields(nullable: true) inArchive(nullable: true) + starredOnly(nullable: true) } } diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SmartGroup.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SmartGroup.groovy index 822e6e766..c186563bd 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SmartGroup.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SmartGroup.groovy @@ -3,7 +3,7 @@ package frontlinesms2 class SmartGroup { //> STATIC PROPERTIES static final String shortName = 'smartgroup' - + static configFields = ['mobile', 'contactName', 'email', 'notes'] //> SMART GROUP PROPERTIES /** the name of this smart group itself. This is mandatory. */ String name @@ -34,6 +34,10 @@ class SmartGroup { def getMembers() { getMembersByName(null, [:]) } + + def countMembers() { + return countMembersByName(null) + } def getMembersByName(String searchString, Map pageParams) { def query = getMembersByNameQuery(searchString) diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SmppFconnection.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SmppFconnection.groovy new file mode 100644 index 000000000..fbd945222 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SmppFconnection.groovy @@ -0,0 +1,75 @@ +package frontlinesms2 + +import frontlinesms2.camel.smpp.* + +import org.apache.camel.builder.RouteBuilder +import org.apache.camel.model.RouteDefinition +import frontlinesms2.camel.exception.* + +class SmppFconnection extends Fconnection { + static final configFields = [name:null, url:null, port:null, username:null, password:null, fromNumber:null, send:null , receive:null] + static defaultValues = [send:true, receive:true] + static String getShortName() { 'smpp' } + + String url + String port + String username + String password + String fromNumber + Boolean send = true + Boolean receive = true + + static constraints = { + url blank:false + port blank:false + username blank:false + password blank:false + fromNumber blank:true + } + + static passwords = [] + + static mapping = { + password column: 'smpp_password' + } + + List getRouteDefinitions() { + return new RouteBuilder() { + @Override void configure() {} + List getRouteDefinitions() { + def definitions = [] + + if(SmppFconnection.this.send){ + definitions << from("seda:out-${SmppFconnection.this.id}") + .setHeader("CamelSmppSourceAddr", simple(SmppFconnection.this.fromNumber)) + .onException(RuntimeException) + .handled(false) + .beanRef('fconnectionService', 'handleDisconnection') + .end() + .setHeader(Fconnection.HEADER_FCONNECTION_ID, simple(SmppFconnection.this.id.toString())) + .process(new SmppPreProcessor()) + .to(SmppFconnection.this.sendingUrl) + .process(new SmppPostProcessor()) + .routeId("out-internet-${SmppFconnection.this.id}") + } + + if(SmppFconnection.this.receive){ + definitions << from(SmppFconnection.this.receivingUrl) + .setHeader(Fconnection.HEADER_FCONNECTION_ID, simple(SmppFconnection.this.id.toString())) + .beanRef('smppTranslationService', 'process') + .to('seda:incoming-fmessages-to-store') + .routeId("in-${SmppFconnection.this.id}") + } + return definitions + } + }.routeDefinitions + } + + private getSendingUrl(){ + return "smpp://${this.username}@${this.url}:${this.port}?password=${this.password}&enquireLinkTimer=30000&transactionTimer=50000&systemType=producer" + } + + private getReceivingUrl(){ + return "smpp://${this.username}@${this.url}:${this.port}?password=${this.password}&enquireLinkTimer=30000&transactionTimer=50000&systemType=consumer" + } +} diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SmslibFconnection.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SmslibFconnection.groovy index b1695d024..a3aa994b5 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SmslibFconnection.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SmslibFconnection.groovy @@ -7,18 +7,22 @@ import org.apache.camel.model.RouteDefinition import org.smslib.NotConnectedException class SmslibFconnection extends Fconnection { - static passwords = ['pin'] - static configFields = ['name', 'port', 'baud', 'pin', 'imsi', 'serial', 'send', 'receive'] - static defaultValues = [send:true, receive:true] + static passwords = [] + static configFields = ['name', 'manufacturer', 'model', 'port', 'baud', 'pin', 'imsi', 'serial', 'sendEnabled', 'receiveEnabled'] + static defaultValues = [sendEnabled:true, receiveEnabled:true, baud:9600] static String getShortName() { 'smslib' } + + def deviceDetectionService private def camelAddress = { def optional = { name, val -> return val? "&$name=$val": '' } - "smslib:$port?debugMode=true&baud=$baud${optional('pin', pin)}&allMessages=$allMessages" + "smslib:$port?debugMode=true&baud=$baud${optional('pin', pin)}&allMessages=$allMessages${optional('manufacturer', manufacturer)}${optional('model', model)}" } + String manufacturer + String model String port int baud String serial @@ -26,23 +30,25 @@ class SmslibFconnection extends Fconnection { String pin // FIXME maybe encode this rather than storing plaintext(?) boolean allMessages = true // TODO rename sendEnabled - boolean send = true + boolean sendEnabled = true // TODO rename receiveEnabled - boolean receive = true + boolean receiveEnabled = true static constraints = { + manufacturer nullable:true + model nullable:true port blank:false - imsi(nullable: true) - pin(nullable: true) - serial(nullable: true) - send(nullable:true, validator: { val, obj -> + imsi nullable:true + pin nullable:true + serial nullable:true + sendEnabled(nullable:true, validator: { val, obj -> if(!val) { - return obj.receive + return obj.receiveEnabled } }) - receive(nullable:true, validator: { val, obj -> + receiveEnabled(nullable:true, validator: { val, obj -> if(!val) { - return obj.send + return obj.sendEnabled } }) } @@ -67,13 +73,23 @@ class SmslibFconnection extends Fconnection { } } } + + def getCustomStatus() { + if(deviceDetectionService.isConnecting(port)) { + return ConnectionStatus.CONNECTING + } + if(fconnectionService.isFailed(this)) { + return ConnectionStatus.FAILED + } + return ConnectionStatus.NOT_CONNECTED + } List getRouteDefinitions() { return new RouteBuilder() { @Override void configure() {} List getRouteDefinitions() { def definitions = [] - if(isSend()) { + if(isSendEnabled()) { definitions << from("seda:out-${SmslibFconnection.this.id}") .onException(NotConnectedException) .handled(true) @@ -84,7 +100,7 @@ class SmslibFconnection extends Fconnection { .to(camelAddress()) .routeId("out-modem-${SmslibFconnection.this.id}") } - if(isReceive()) { + if(isReceiveEnabled()) { definitions << from(camelAddress()) .onException(NotConnectedException) .handled(true) @@ -99,3 +115,4 @@ class SmslibFconnection extends Fconnection { }.routeDefinitions } } + diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SmssyncFconnection.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SmssyncFconnection.groovy index aaac80717..699924c43 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SmssyncFconnection.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SmssyncFconnection.groovy @@ -2,29 +2,40 @@ package frontlinesms2 import org.apache.camel.builder.RouteBuilder import org.apache.camel.model.RouteDefinition +import org.apache.camel.Exchange import frontlinesms2.api.* @FrontlineApiAnnotations(apiUrl="smssync") class SmssyncFconnection extends Fconnection implements FrontlineApi { static String getShortName() { 'smssync' } - static final configFields = ['name', 'receiveEnabled', 'sendEnabled', 'secret'] - static final passwords = ['secret'] - static final defaultValues = [sendEnabled:true, receiveEnabled:true] + static final configFields = ['info-setup': ['secret'], 'info-timeout':['timeout'], 'info-name':['name']] + static final passwords = [] + static final defaultValues = [timeout:60] def smssyncService def appSettingsService + def grailsLinkGenerator + def urlHelperService + def dispatchRouterService + Date lastConnectionTime boolean sendEnabled = true boolean receiveEnabled = true + boolean hasDispatches = false String secret + int timeout = 360 // 3 hour default static constraints = { secret nullable:true + lastConnectionTime nullable:true } - def removeDispatchesFromQueue(dispatches) { - SmssyncFconnectionQueuedDispatch.delete(this, dispatches) + def removeDispatchesFromQueue() { + QueuedDispatch.deleteAll(this) + if(this.hasDispatches) { + this.hasDispatches = false + } } def apiProcess(controller) { @@ -32,11 +43,15 @@ class SmssyncFconnection extends Fconnection implements FrontlineApi { } def addToQueuedDispatches(d) { - SmssyncFconnectionQueuedDispatch.create(this, d) + QueuedDispatch.create(this, d) } def getQueuedDispatches() { - SmssyncFconnectionQueuedDispatch.getDispatches(this) + QueuedDispatch.getDispatches(this) + } + + def updateDispatch(Exchange x) { + // Dispatch is already in PENDING state so no need to change the status } boolean isApiEnabled() { return this.sendEnabled || this.receiveEnabled } @@ -58,8 +73,10 @@ class SmssyncFconnection extends Fconnection implements FrontlineApi { return routeDefinitions } - String getFullApiUrl() { - return apiEnabled? "http://[your-ip-address]:${appSettingsService.serverPort}/frontlinesms-core/api/1/$apiUrl/$id/" : "" + String getFullApiUrl(request) { + def entityClassApiUrl = SmssyncFconnection.getAnnotation(FrontlineApiAnnotations.class)?.apiUrl() + def path = grailsLinkGenerator.link(controller:'api', params:[entityClassApiUrl:entityClassApiUrl, entityId:id, secret:secret], absolute:false) + return apiEnabled? "${urlHelperService.getBaseUrl(request)}$path" :'' } } diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Step.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Step.groovy new file mode 100644 index 000000000..6571c7850 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Step.groovy @@ -0,0 +1,46 @@ +package frontlinesms2 + +import grails.converters.JSON + +abstract class Step { + + def i18nUtilService + + static belongsTo = [activity: CustomActivity] + static hasMany = [stepProperties: StepProperty] + static def implementations = [JoinActionStep, LeaveActionStep, ReplyActionStep, WebconnectionActionStep, ForwardActionStep] + + static transients = ['i18nUtilService'] + static configFields = [:] + + static constraints = { + stepProperties(nullable: true) + } + + static mapping = { + stepProperties cascade: "all-delete-orphan" + } + + abstract def process(TextMessage message) + + String getPropertyValue(key) { + stepProperties?.find { it.key == key }?.value + } + + def setPropertyValue(key, value){ + def prop = stepProperties?.find { it.key == key } + prop? (prop.value = value) : this.addToStepProperties(key:key, value:value) + } + + // helper method to retrieve list of entities saved as StepProperties + def getEntityList(entityType, propertyName) { + entityType.getAll(StepProperty.findAllByStepAndKey(this, propertyName)*.value) - null + } + + String getJsonConfig() { + return getConfig() as JSON + } + + def activate() {} + def deactivate() {} +} diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/StepProperty.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/StepProperty.groovy new file mode 100644 index 000000000..1b8cc8e91 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/StepProperty.groovy @@ -0,0 +1,7 @@ +package frontlinesms2 + +class StepProperty { + static belongsTo = [step: Step] + String key + String value +} diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Subscription.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Subscription.groovy index 475bf334b..0a593c0e8 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Subscription.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Subscription.groovy @@ -1,6 +1,6 @@ package frontlinesms2 -class Subscription extends Activity{ +class Subscription extends Activity { //> CONSTANTS static String getShortName() { 'subscription' } @@ -11,6 +11,7 @@ class Subscription extends Activity{ Action defaultAction = Action.TOGGLE String joinAutoreplyText String leaveAutoreplyText + def subscriptionService //> SERVICES def messageSendService @@ -27,41 +28,25 @@ class Subscription extends Activity{ leaveAutoreplyText nullable:true, blank:false } - def processJoin(Fmessage message){ + def processJoin(TextMessage message){ + println "I AM ABOUT TO CALL DO JOIN ON $subscriptionService" this.addToMessages(message) - this.save() - message.ownerDetail = Action.JOIN.toString() - message.save(failOnError:true) - withEachCorrespondent(message, { phoneNumber -> - println "##### >>>>> ${Contact.findByMobile(phoneNumber)}" - def foundContact = Contact.findByMobile(phoneNumber) - if(!foundContact) { - foundContact = new Contact(name:"", mobile:phoneNumber).save(failOnError:true) - group.addToMembers(foundContact); - } else { - if(!(foundContact.isMemberOf(group))){ - group.addToMembers(foundContact); - } - } - if(joinAutoreplyText) { - sendAutoreplyMessage(foundContact, joinAutoreplyText) - } - }) + this.save(failOnError:true) + subscriptionService.doJoin(this, message) } - def processLeave(Fmessage message){ + def processLeave(TextMessage message){ this.addToMessages(message) - this.save() - message.ownerDetail = Action.LEAVE.toString() - message.save(failOnError:true) - withEachCorrespondent(message, { phoneNumber -> - println "##### >>>>> ${Contact.findByMobile(phoneNumber)}" - def foundContact = Contact.findByMobile(phoneNumber) - foundContact?.removeFromGroup(group) - if(leaveAutoreplyText && foundContact) { - sendAutoreplyMessage(foundContact, leaveAutoreplyText) - } - }) + this.save(failOnError:true) + println "I AM ABOUT TO CALL DO LEAVE ON $subscriptionService" + subscriptionService.doLeave(this, message) + } + + def processToggle(TextMessage message){ + this.addToMessages(message) + this.save(failOnError:true) + println "I AM ABOUT TO CALL DO TOGGLE ON $subscriptionService" + subscriptionService.doToggle(this, message) } def sendAutoreplyMessage(Contact foundContact, autoreplyText) { @@ -74,36 +59,11 @@ class Subscription extends Activity{ messageSendService.send(outgoingMessage) } - // TODO this should just call processJoin or processLeave - def processToggle(Fmessage message){ + def processKeyword(TextMessage message, Keyword k) { + // TODO: Should add message to activity at this point this.addToMessages(message) - this.save() - message.ownerDetail = Action.TOGGLE.toString() - message.save(failOnError:true) - withEachCorrespondent(message, { phoneNumber -> - def foundContact = Contact.findByMobile(phoneNumber) - if(foundContact){ - if(foundContact.isMemberOf(group)) { - foundContact.removeFromGroup(group) - if(leaveAutoreplyText) - sendAutoreplyMessage(foundContact, leaveAutoreplyText) - } else { - group.addToMembers(foundContact); - if(joinAutoreplyText) - sendAutoreplyMessage(foundContact, joinAutoreplyText) - } - } else { - foundContact = new Contact(name:"", mobile:phoneNumber).save(failOnError:true) - group.addToMembers(foundContact); - if(joinAutoreplyText) - sendAutoreplyMessage(foundContact, joinAutoreplyText) - } - }) - } - - def processKeyword(Fmessage message, Keyword k) { + this.save(failOnError:true) def action = getAction(k) - message.ownerDetail = action.toString() if(action == Action.JOIN){ processJoin(message) }else if(action == Action.LEAVE) { @@ -114,13 +74,13 @@ class Subscription extends Activity{ } Action getAction(Keyword k) { - def actionText = k.ownerDetail - println "### OwnerDetail ## ${k.ownerDetail}" + def actionText = k?.ownerDetail + println "### OwnerDetail ## ${k?.ownerDetail}" if(actionText == Action.JOIN.toString()){ return Action.JOIN } else if(actionText == Action.LEAVE.toString()){ return Action.LEAVE - }else if(actionText == null){ + }else { return defaultAction } } @@ -129,14 +89,14 @@ class Subscription extends Activity{ aliases && aliases.toUpperCase().split(",").contains(message.substring(keyword.value.length())) } - def getDisplayText(Fmessage msg) { - if ((msg.messageOwner.id == this.id) && msg.ownerDetail) { + def getDisplayText(TextMessage msg) { + if ((msg.messageOwner.id == this.id) && msg.ownerDetail && msg.inbound) { return (msg.ownerDetail?.toLowerCase() + ' ("' + msg.text + '")').truncate(50) // FIXME probably shouldn't truncate here } else return msg.text } - def withEachCorrespondent(Fmessage message, Closure c) { + def withEachCorrespondent(TextMessage message, Closure c) { def phoneNumbers = [] if (message.inbound) phoneNumbers << message.src diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SystemNotification.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SystemNotification.groovy index 4fdc48348..9e45a3159 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SystemNotification.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/SystemNotification.groovy @@ -1,15 +1,27 @@ package frontlinesms2 class SystemNotification { +//> CONSTANTS private static final int MAX_TEXT_LENGTH = 511 + +//> INSTANCE PROPERTIES String text - boolean read - + boolean rd + String topic + +//> DOMAIN SETUP + static transients = ['read'] static constraints = { - text(blank:false, maxSize:MAX_TEXT_LENGTH) + text blank:false, maxSize:MAX_TEXT_LENGTH + topic nullable:true } +//> ACCESSORS public void setText(String text) { this.text = text?.truncate(MAX_TEXT_LENGTH) } + + public boolean isRead() { return this.rd } + public boolean setRead(boolean read) { this.rd = read } } + diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/TextMessage.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/TextMessage.groovy new file mode 100644 index 000000000..ccc3783a6 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/TextMessage.groovy @@ -0,0 +1,256 @@ +package frontlinesms2 + +import groovy.time.* + +class TextMessage extends Interaction { + def mobileNumberUtilService + + static final int MAX_TEXT_LENGTH = 1600 + static final String TEST_MESSAGE_TEXT = "Test Message" + + String text + + boolean inbound + + static hasMany = [dispatches:Dispatch, details:MessageDetail] + + static constraints = { + text maxSize:MAX_TEXT_LENGTH + inboundContactName nullable:true + outboundContactName nullable:true + inbound(nullable:true, validator: { val, obj -> + val ^ (obj.dispatches? true: false) + }) + dispatches nullable:true + details nullable:true + } + + static namedQueries = { + inbox { getOnlyStarred=false, archived=false -> + and { + eq("isDeleted", false) + eq("archived", archived) + if(getOnlyStarred) + eq("starred", true) + eq("inbound", true) + isNull("messageOwner") + } + } + sent { getOnlyStarred=false, archived=false -> + and { + eq("isDeleted", false) + eq("archived", archived) + if(!archived) { + projections { dispatches { eq('status', DispatchStatus.SENT) } } + } else { + projections { + dispatches { + or { + eq('status', DispatchStatus.SENT) + eq('status', DispatchStatus.PENDING) + eq('status', DispatchStatus.FAILED) + } + } + } + } + if(getOnlyStarred) + eq("starred", true) + } + } + pending { getOnlyFailed=false -> + and { + eq("isDeleted", false) + eq("archived", false) + if(getOnlyFailed) { + projections { dispatches { eq('status', DispatchStatus.FAILED) } } + } else { + projections { + dispatches { + or { + eq('status', DispatchStatus.PENDING) + eq('status', DispatchStatus.FAILED) + } + } + } + } + } + projections { + distinct 'id' + property 'date' + property 'id' + } + } + + pendingAndNotFailed { + and { + eq("isDeleted", false) + eq("archived", false) + projections { + dispatches { + eq('status', DispatchStatus.PENDING) + } + } + } + projections { + distinct 'id' + property 'date' + property 'id' + } + } + + forReceivedStats { params -> + def groupInstance = params.groupInstance + def messageOwner = params.messageOwner + def startDate = params.startDate.startOfDay + def endDate = params.endDate.endOfDay + + and { + eq('inbound', true) + eq('isDeleted', false) + between("date", startDate, endDate) + if(groupInstance) 'in'('src', groupInstance?.addresses ?: "") + if(messageOwner) 'in'('messageOwner', messageOwner) + } + } + } << Interaction.namedQueries // Named Queries are not inherited + + def getDisplayName() { + if(inbound) { + if(inboundContactName) return inboundContactName + else if(id) return src.toPrettyPhoneNumber() + else return Contact.findByMobile(src)?.name?: src.toPrettyPhoneNumber() + } else if(dispatches.size() == 1) { + if(outboundContactName) return outboundContactName + else { + def dst = (dispatches as List)[0].dst + if(id) return dst.toPrettyPhoneNumber() + else return Contact.findByMobile(dst)?.name?: dst.toPrettyPhoneNumber() + } + } else { + return Integer.toString(dispatches.size()) + } + } + + def getContactFlagCSSClasses() { + def flagCssClass + if(inbound) { + flagCssClass = mobileNumberUtilService.getFlagCSSClasses(src) + } else if(dispatches.size() == 1) { + def dst = (dispatches as List)[0].dst + flagCssClass = mobileNumberUtilService.getFlagCSSClasses(dst) + } + flagCssClass + } + + def getHasSent() { areAnyDispatches(DispatchStatus.SENT) } + def getHasFailed() { areAnyDispatches(DispatchStatus.FAILED) } + def getHasPending() { areAnyDispatches(DispatchStatus.PENDING) } + def getOutboundContactList() { + dispatches.collect { Contact.findByMobile(it.dst)?.name } - null + } + + private def areAnyDispatches(status) { + !inbound && dispatches?.any { it.status == status } + } + + public void setText(String text) { + this.text = text?.truncate(MAX_TEXT_LENGTH) + } + + static def listPending(onlyFailed, params=[:]) { + def ids = pending(onlyFailed).list(params) as List + (!ids) ? [] : TextMessage.getAll(ids) + } + static def countUnreadMessages() { + TextMessage.unread.count() + } + + static def countPending(onlyFailed) { + pending(onlyFailed).list().size() + } + + static def hasFailedMessages() { + return pending(true).count() > 0 + } + + static def countAllMessages() { + ['inbox', 'sent', 'pending', 'deleted'].collectEntries { [it, TextMessage[it].count()] } + } + + // TODO should this be in a service? + static def getMessageStats(params) { + def asKey = { date -> date.format('dd/MM') } + + def dates = [:] + (params.startDate..params.endDate).each { date -> + dates[asKey(date)] = [sent:0, received:0] + } + + if(!params.inbound) { + // TODO the named query should ideally do the counts for us + Dispatch.forSentStats(params).list().each { d -> + ++dates[asKey(d.dateSent)].sent + } + } + + if(params.inbound == null || params.inbound) { + // TODO the named query should ideally do the counts for us + TextMessage.forReceivedStats(params).list().each { m -> + ++dates[asKey(m.date)].received + } + } + + dates + } + + def updateDispatches() { + if(isDeleted) { + dispatches.each { + it.isDeleted = true + } + } + } + + def getMessageDetailValue(owner) { + def ownerType = getOwnerType(owner) + if(owner && (Activity.get(owner?.id) instanceof CustomActivity)) { + def stepId = this.details.find { it.ownerType == ownerType && it.ownerId == owner.id }?.value + def t = this.details.find { (it.ownerType == MessageDetail.OwnerType.STEP) && (it.ownerId == stepId as Long) }?.value + return t + } else { + return this.details?.find { it.ownerType == ownerType && it.ownerId == owner?.id }?.value?:'' + } + } + + //> GETTER AND SETTER OF MESSAGE DETAIL THAT USE CURRENT MESSAGE OWNER + def getOwnerDetail() { + messageOwner ? getMessageDetailValue(this.messageOwner) : null + } + + def setMessageDetail(activityOrStep, val) { + if (activityOrStep instanceof Activity) { + this.setMessageDetailValue(activityOrStep, val) + } else { + this.setMessageDetailValue(this.messageOwner, activityOrStep.id) + this.setMessageDetailValue(activityOrStep, val) + } + } + + private def setMessageDetailValue(owner, value) { + def ownerType = getOwnerType(owner) + def messageDetailInstance = this.details.find { it.ownerType == ownerType && it.ownerId == owner.id } + if(!messageDetailInstance) { + messageDetailInstance = new MessageDetail(ownerType:ownerType, ownerId:owner.id) + this.addToDetails(messageDetailInstance) + } + messageDetailInstance.value = value + this.save(failOnError:true) + messageDetailInstance.save(failOnError:true) + } + + def clearAllDetails() { + this.details?.clear() + } + +} + diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Trash.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Trash.groovy index 48259c627..fe38b5e80 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Trash.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Trash.groovy @@ -1,18 +1,18 @@ package frontlinesms2 -import frontlinesms2.Fmessage - class Trash { Date dateCreated Long objectId String objectClass String displayName String displayText + + static final int MAXIMUM_DISPLAY_TEXT_SIZE = 255 static constraints = { objectId unique:'objectClass' displayName(nullable: true) - displayText(nullable: true) + displayText(nullable: true, size: 0..MAXIMUM_DISPLAY_TEXT_SIZE) } static def findByObject(def o) { diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/UshahidiWebconnection.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/UshahidiWebconnection.groovy index f18810432..e2ad2d567 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/UshahidiWebconnection.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/UshahidiWebconnection.groovy @@ -1,7 +1,5 @@ package frontlinesms2 -import org.apache.camel.Exchange - class UshahidiWebconnection extends Webconnection { static String getType() { 'ushahidi' } static constraints = { @@ -16,8 +14,8 @@ class UshahidiWebconnection extends Webconnection { this.httpMethod = Webconnection.HttpMethod.GET this.name = params.name // API setup - this.apiEnabled = params.enableApi?:false - this.secret = params.secret + this.apiEnabled = false //params.enableApi?:false <- replace with this to re-enable in future + // this.secret = params.secret <- uncomment to re-enable in future this } diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Webconnection.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Webconnection.groovy index fc6d02855..4fde34bd5 100644 --- a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Webconnection.groovy +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/Webconnection.groovy @@ -12,10 +12,25 @@ abstract class Webconnection extends Activity implements FrontlineApi { static final String OWNERDETAIL_SUCCESS = 'success' static final String OWNERDETAIL_PENDING = 'pending' static final String OWNERDETAIL_FAILED = 'failed' - + + static subFields = ['message_body' : { msg -> + def keyword = msg.messageOwner?.keywords?.find{ msg.text.toUpperCase().startsWith(it.value) }?.value + def text = msg.text + if (keyword?.size() && text.toUpperCase().startsWith(keyword.toUpperCase())) { + text = text.substring(keyword.size()).trim() + } + text + }, + 'message_body_with_keyword' : { msg -> msg.text }, + 'message_src_number' : { msg -> msg.src }, + 'message_src_name' : { msg -> Contact.findByMobile(msg.src)?.name ?: msg.src }, + 'message_timestamp' : { msg -> msg.dateCreated }] + def camelContext def webconnectionService def appSettingsService + def urlHelperService + def grailsLinkGenerator enum HttpMethod { POST, GET } static String shortName = 'webconnection' static String getType() { '' } @@ -26,20 +41,6 @@ abstract class Webconnection extends Activity implements FrontlineApi { static final def retryAttempts = 3 // how many times to retry before giving up static final def initialRetryDelay = 10000 // delay before first retry, in milliseconds static final def delayMultiplier = 3 // multiplier applied to delay after each failed attempt - - // Substitution variables - static subFields = ['message_body' : { msg -> - def keyword = msg.messageOwner?.keywords?.find{ msg.text.toUpperCase().startsWith(it.value) }?.value - def text = msg.text - if (keyword?.size() && text.toUpperCase().startsWith(keyword.toUpperCase())) { - text = text.substring(keyword.size()).trim() - } - text - }, - 'message_body_with_keyword' : { msg -> msg.text }, - 'message_src_number' : { msg -> msg.src }, - 'message_src_name' : { msg -> Contact.findByMobile(msg.src)?.name ?: msg.src }, - 'message_timestamp' : { msg -> msg.dateCreated }] /// Variables String url @@ -57,26 +58,31 @@ abstract class Webconnection extends Activity implements FrontlineApi { return true }) secret(nullable:true) - url(nullable:false, validator: { val, obj -> - return val.startsWith("http://") || val.startsWith("https://") - }) + url nullable:false, url:true, validator:{ val, obj -> + if(!(val ==~ 'http(s?)://.*')) { + return "webconnection.url.error.url.start.with.http" + } + if(val.toLowerCase() ==~ 'http(s?)://localhost(:[0-9]+)?(\\/.*)?') { + return "webconnection.url.error.locahost.invalid.use.ip" + } + } } static mapping = { requestParameters cascade: "all-delete-orphan" tablePerHierarchy false } - def processKeyword(Fmessage message, Keyword k) { + def processKeyword(TextMessage message, Keyword k) { this.addToMessages(message) this.save(failOnError:true) - webconnectionService.send(message) + webconnectionService.doUpload(this, message) } List getRouteDefinitions() { return new RouteBuilder() { @Override void configure() {} List getRouteDefinitions() { - return [from("seda:activity-webconnection-${Webconnection.this.id}") + return [from("seda:activity-${Webconnection.shortName}-${Webconnection.this.id}") .beanRef('webconnectionService', 'preProcess') .setHeader(Exchange.HTTP_PATH, simple('${header.url}')) .onException(Exception) @@ -89,37 +95,18 @@ abstract class Webconnection extends Activity implements FrontlineApi { .end() .to(Webconnection.this.url) .beanRef('webconnectionService', 'postProcess') - .routeId("activity-webconnection-${Webconnection.this.id}")] + .routeId("activity-${Webconnection.shortName}-${Webconnection.this.id}")] } }.routeDefinitions } def activate() { - try { - deactivate() - } catch(Exception ex) { - log.info("Exception thrown while deactivating webconnection '$name'", ex) - } - println "*** ACTIVATING ACTIVITY ***" - try { - def routes = this.routeDefinitions - camelContext.addRouteDefinitions(routes) - println "################# Activating Webconnection :: ${this}" - LogEntry.log("Created Webconnection routes: ${routes*.id}") - } catch(FailedToCreateProducerException ex) { - println ex - } catch(Exception ex) { - println ex - camelContext.stopRoute("activity-webconnection-${this.id}") - camelContext.removeRoute("activity-webconnection-${this.id}") - } + webconnectionService.activate(this) } def deactivate() { - println "################ Deactivating Webconnection :: ${this}" - camelContext.stopRoute("activity-webconnection-${this.id}") - camelContext.removeRoute("activity-webconnection-${this.id}") + webconnectionService.deactivate(this) } abstract def initialize(params) @@ -129,9 +116,9 @@ abstract class Webconnection extends Activity implements FrontlineApi { println "x: ${x}" println "x.in: ${x.in}" println "x.in.headers: ${x.in.headers}" - def fmessage = Fmessage.get(x.in.headers.'fmessage-id') + def textMessage = TextMessage.get(x.in.headers.'fmessage-id') def encodedParameters = this.requestParameters.collect { - urlEncode(it.name) + '=' + urlEncode(it.getProcessedValue(fmessage)) + urlEncode(it.name) + '=' + urlEncode(webconnectionService.getProcessedValue(it, textMessage)) }.join('&') println "PARAMS:::$encodedParameters" x.in.headers[Exchange.HTTP_PATH] = this.url @@ -150,7 +137,7 @@ abstract class Webconnection extends Activity implements FrontlineApi { println "x.in.body = $x.in.body" } - def postProcess(Exchange x) { + void postProcess(Exchange x) { println "###### Webconnection.postProcess() with Exchange # ${x}" println "Web Connection Response::\n ${x.in.body}" log.info "Web Connection Response::\n ${x.in.body}" @@ -167,8 +154,12 @@ abstract class Webconnection extends Activity implements FrontlineApi { webconnectionService.apiProcess(this, controller) } - String getFullApiUrl() { - return apiEnabled? "http://[your-ip-address]:${appSettingsService.serverPort}/frontlinesms-core/api/1/$apiUrl/$id/" : "" + def getMoreActions() { ['retryFailed'] } + + String getFullApiUrl(request) { + def entityClassApiUrl = Webconnection.getAnnotation(FrontlineApiAnnotations.class)?.apiUrl() + def path = grailsLinkGenerator.link(controller: 'api', params:[entityClassApiUrl: entityClassApiUrl, entityId: id], absolute: false) + return apiEnabled? "${urlHelperService.getBaseUrl(request)}$path" : "" } } diff --git a/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/WebconnectionActionStep.groovy b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/WebconnectionActionStep.groovy new file mode 100644 index 000000000..2b40a879c --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/domain/frontlinesms2/WebconnectionActionStep.groovy @@ -0,0 +1,127 @@ +package frontlinesms2 + +import org.apache.camel.* +import org.apache.camel.Exchange +import org.apache.camel.builder.RouteBuilder +import org.apache.camel.model.RouteDefinition +import frontlinesms2.camel.exception.* + +class WebconnectionActionStep extends Step { + def webconnectionService + static service = 'webconnection' + static action = 'doUpload' + static String getShortName() { 'webconnectionStep' } + + static configFields = [httpMethod: [Webconnection.HttpMethod.GET, Webconnection.HttpMethod.POST], url:'url', params:[:]] + + static constraints = { + } + + Map getConfig() { + [stepId:id, httpMethod:getHttpMethod(), urlEncode:getUrl(), requestParameters:getRequestParameters()] + } + + def getHttpMethod() { + getPropertyValue("httpMethod") + } + + def setHttpMethod(String method) { + setPropertyValue("httpMethod", method) + } + + String getUrl() { + getPropertyValue("url") + } + + def setPropertyValue(key, value) { + super.setPropertyValue("${key in ['url', 'httpMethod']?'':'param:'}$key", value) + } + + def getRequestParameters() { + def parameters = [] + this.stepProperties?.each { property-> + if(property.key.startsWith("param:")) { + parameters << [name:property.key.split("param:", 2)[1], value:property.value] + } + } + println "## webconnectionStep.params # ${parameters}" + return parameters + } + + def process(TextMessage message) { + webconnectionService.doUpload(this, message) + } + + def getDescription() { + i18nUtilService.getMessage(code:"customactivity.${this.shortName}.description", args:[this.url]) + } + + List getRouteDefinitions() { + return new RouteBuilder() { + @Override void configure() {} + List getRouteDefinitions() { + return [from("seda:activity-${WebconnectionActionStep.shortName}-${WebconnectionActionStep.this.id}") + .beanRef('webconnectionService', 'preProcess') + .setHeader(Exchange.HTTP_PATH, simple('${header.url}')) + .onException(Exception) + .redeliveryDelay(Webconnection.initialRetryDelay) + .backOffMultiplier(Webconnection.delayMultiplier) + .maximumRedeliveries(Webconnection.retryAttempts) + .retryAttemptedLogLevel(LoggingLevel.WARN) + .handled(true) + .beanRef('webconnectionService', 'handleException') + .end() + .to(WebconnectionActionStep.this.url) + .beanRef('webconnectionService', 'postProcess') + .routeId("activity-${WebconnectionActionStep.shortName}-${WebconnectionActionStep.this.id}")] + } + }.routeDefinitions + } + + def activate() { + webconnectionService.activate(this) + } + + def deactivate() { + webconnectionService.deactivate(this) + } + + def preProcess(Exchange x) { + println "x: ${x}" + println "x.in: ${x.in}" + println "x.in.headers: ${x.in.headers}" + def textMessage = TextMessage.get(x.in.headers.'fmessage-id') + def stepProperties = this.requestParameters.collect { if(!(it.key ==~ /httpMethod|url/)) it} - null + def encodedParameters = stepProperties?.collect { + urlEncode(it.name) + '=' + urlEncode(webconnectionService.getProcessedValue(it, textMessage)) + }.join('&') + println "PARAMS:::$encodedParameters" + x.in.headers[Exchange.HTTP_PATH] = this.url + x.in.headers[Exchange.HTTP_METHOD] = this.httpMethod + switch(this.httpMethod) { + case 'GET': + x.in.headers[Exchange.HTTP_QUERY] = encodedParameters + break; + case 'POST': + x.in.body = encodedParameters + x.in.headers[Exchange.CONTENT_TYPE] = 'application/x-www-form-urlencoded' + break; + } + println "# Exchange after adding other headers in the Webconnection.preProcess()" + println "x.in.headers = $x.in.headers" + println "x.in.body = $x.in.body" + } + + void postProcess(Exchange x) { + println "###### WebconnectionActionStep.postProcess() with Exchange # ${x}" + println "Web Connection ActionStep Response::\n ${x.in.body}" + log.info "Web Connection ActionStep Response::\n ${x.in.body}" + } + + private String urlEncode(String s) throws UnsupportedEncodingException { + println "PreProcessor.urlEncode : s is $s" + println "PreProcessor.urlEncode : s=$s -> ${URLEncoder.encode(s, "UTF-8")}" + return URLEncoder.encode(s, "UTF-8"); + } + +} diff --git a/plugins/frontlinesms-core/grails-app/i18n/messages.properties b/plugins/frontlinesms-core/grails-app/i18n/messages.properties index 597e661cb..8c708a4f8 100644 --- a/plugins/frontlinesms-core/grails-app/i18n/messages.properties +++ b/plugins/frontlinesms-core/grails-app/i18n/messages.properties @@ -1,9 +1,7 @@ # FrontlineSMS English translation by the FrontlineSMS team, Nairobi language.name=English - # General info app.version.label=Version - # Common action imperatives - to be used for button labels and similar action.ok=OK action.close=Close @@ -13,70 +11,79 @@ action.next=Next action.prev=Previous action.back=Back action.create=Create -action.edit=Edit +action.edit=Update action.rename=Rename action.save=Save -action.save.all=Save All +action.save.all=Save Selected action.delete=Delete -action.delete.all=Delete All +action.delete.all=Delete Selected action.send=Send action.export=Export - +action.import=Import +action.view=View +contact.status.unsaved=There are unsaved changes +contact.status.saved=Contact saved at {0} +contact.status.saving=Saving ... +content.loading=Loading... # Messages when FrontlineSMS server connection is lost server.connection.fail.title=Connection to the server has been lost. server.connection.fail.info=Please restart FrontlineSMS, or close this window. - #Connections: -connection.creation.failed=Connection could not be created {0} -connection.route.destroyed=Destroyed route from {0} to {1} -connection.route.connecting=Connecting... -connection.route.disconnecting=Disconnecting... -connection.route.successNotification=Successfully created route on {0} -connection.route.failNotification=Failed to create route on {1}: {2} [edit] -connection.route.destroyNotification=Disconnected route on {0} -connection.test.sent=Test message successfully sent to {0} using {1} +connection.creation.failed=Connection could not be created. {0} +connection.route.disabled=Deleted connection from {0} to {1}. +connection.route.successNotification=Successfully created connection on {0}. +connection.route.failNotification=Failed to create connection on {1}: {2} [[[edit]](({0}))]. +connection.route.disableNotification=Disconnected connection on {0} +connection.route.pauseNotification=Paused connection on {0} +connection.route.resumeNotification=Resumed connection on {0} +connection.test.sent=Your test message was successfully sent to {0} using {1}. connection.route.exception={1} - # Connection exception messages -connection.error.org.smslib.alreadyconnectedexception=Device already connected -connection.error.org.smslib.gsmnetworkregistrationexception=Failed to register with GSM network +connection.error.org.smslib.alreadyconnectedexception=Device already connected. +connection.error.org.smslib.gsmnetworkregistrationexception=Failed to register with GSM network. connection.error.org.smslib.invalidpinexception=Incorrect PIN supplied connection.error.org.smslib.nopinexception=PIN required but not supplied +connection.error.org.smslib.notconnectedexception={0} +connection.error.org.smslib.nosuchportexception=Port not found, or not accessible connection.error.java.io.ioexception=Port threw an error: {0} -connection.error.frontlinesms2.camel.exception.invalidapiidexception= {0} -connection.error.frontlinesms2.camel.exception.authenticationexception= {0} +connection.error.frontlinesms2.camel.exception.invalidapiidexception={0} +connection.error.frontlinesms2.camel.exception.authenticationexception={0} connection.error.frontlinesms2.camel.exception.insufficientcreditexception={0} - -connection.header=Connections +connection.error.serial.nosuchportexception=Port cannot be found +connection.error.org.apache.camel.runtimecamelexception=Cannot establish the connection. +connection.error.onsave={0} +connection.header=Settings > Connections connection.list.none=You have no connections configured. -connection.edit=Edit Connection -connection.delete=Delete Connection +connection.edit=Edit +connection.delete=Delete connection.deleted=Connection {0} was deleted. -connection.route.create=Create route +connection.route.enable=Enable +connection.route.retryconnection=Retry connection.add=Add new connection connection.createtest.message.label=Message -connection.route.destroy=Destroy route +connection.route.disable=Disable connection.send.test.message=Send test message connection.test.message=Congratulations from FrontlineSMS \\o/ you have successfully configured {0} to send SMS \\o/ connection.validation.prompt=Please fill in all required fields -connection.select=Select connection type +connection.select=Select connection types connection.type=Choose type connection.details=Enter details connection.confirm=Confirm connection.createtest.number=Number connection.confirm.header=Confirm settings connection.name.autoconfigured=Auto-configured {0} {1} on port {2}" - -status.connection.header=Connections +status.connection.title=Connections +status.connection.manage=Manage your connections status.connection.none=You have no connections configured. status.devises.header=Detected devices status.detect.modems=Detect Modems status.modems.none=No devices have been detected yet. - -connectionstatus.not_connected=Not connected +status.header=Usage Statistics connectionstatus.connecting=Connecting connectionstatus.connected=Connected - +connectionstatus.disabled=Disabled +connectionstatus.failed=Failed +connectionstatus.not_connected=Not Connected default.doesnt.match.message=Property [{0}] of class [{1}] with value [{2}] does not match the required pattern [{3}] default.invalid.url.message=Property [{0}] of class [{1}] with value [{2}] is not a valid URL default.invalid.creditCard.message=Property [{0}] of class [{1}] with value [{2}] is not a valid credit card number @@ -89,43 +96,38 @@ default.invalid.max.size.message=Property [{0}] of class [{1}] with value [{2}] default.invalid.min.size.message=Property [{0}] of class [{1}] with value [{2}] is less than the minimum size of [{3}] default.invalid.validator.message=Property [{0}] of class [{1}] with value [{2}] does not pass custom validation default.not.inlist.message=Property [{0}] of class [{1}] with value [{2}] is not contained within the list [{3}] -default.blank.message=Property [{0}] of class [{1}] cannot be blank +default.blank.message=This value cannot be blank default.not.equal.message=Property [{0}] of class [{1}] with value [{2}] cannot equal [{3}] default.null.message=Property [{0}] of class [{1}] cannot be null default.not.unique.message=Property [{0}] of class [{1}] with value [{2}] must be unique - default.paginate.prev=Previous default.paginate.next=Next default.boolean.true=True default.boolean.false=False default.date.format=dd MMMM, yyyy hh:mm default.number.format=0 - default.unarchived={0} unarchived default.unarchive.failed=Unarchiving {0} failed -default.trashed={0} trashed default.restored={0} restored default.restore.failed=Could not restore {0} with id {1} -default.archived={0} archived successfully! default.archived.multiple={0} archived default.created={0} created default.created.message={0} {1} has been created default.create.failed=Failed to create {0} default.updated={0} has been updated default.update.failed=Failed to update {0} with id {1} -default.updated.multiple= {0} have been updated +default.updated.multiple={0} have been updated default.updated.message={0} updated default.deleted={0} deleted +default.deleted.multiple={0} deleted default.trashed={0} moved to trash default.trashed.multiple={0} moved to trash default.archived={0} archived -default.unarchived={0} unarchived default.unarchive.keyword.failed=Unarchiving {0} failed. Keyword or name in use default.unarchived.multiple={0} unarchived default.delete.failed=Could not delete {0} with id {1} default.notfound=Could not find {0} with id {1} default.optimistic.locking.failure=Another user has updated this {0} while you were editing - default.home.label=Home default.list.label={0} List default.add.label=Add {0} @@ -134,7 +136,6 @@ default.create.label=Create {0} default.show.label=Show {0} default.edit.label=Edit {0} search.clear=Clear search - default.button.create.label=Create default.button.edit.label=Edit default.button.update.label=Update @@ -142,87 +143,143 @@ default.button.delete.label=Delete default.button.search.label=Search default.button.apply.label=Apply default.button.delete.confirm.message=Are you sure? - default.deleted.message={0} deleted - # Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) -typeMismatch.java.net.URL=Property {0} must be a valid URL -typeMismatch.java.net.URI=Property {0} must be a valid URI -typeMismatch.java.util.Date=Property {0} must be a valid Date -typeMismatch.java.lang.Double=Property {0} must be a valid number -typeMismatch.java.lang.Integer=Property {0} must be a valid number -typeMismatch.java.lang.Long=Property {0} must be a valid number -typeMismatch.java.lang.Short=Property {0} must be a valid number -typeMismatch.java.math.BigDecimal=Property {0} must be a valid number -typeMismatch.java.math.BigInteger=Property {0} must be a valid number -typeMismatch.int = {0} must be a valid number - +typeMismatch.java.net.URL=Property {0} must be a valid URL. +typeMismatch.java.net.URI=Property {0} must be a valid URI. +typeMismatch.java.util.Date=Property {0} must be a valid date. +typeMismatch.java.lang.Double=Property {0} must be a valid number. +typeMismatch.java.lang.Integer=Property {0} must be a valid number. +typeMismatch.java.lang.Long=Property {0} must be a valid number. +typeMismatch.java.lang.Short=Property {0} must be a valid number. +typeMismatch.java.math.BigDecimal=Property {0} must be a valid number. +typeMismatch.java.math.BigInteger=Property {0} must be a valid number. +typeMismatch.int={0} must be a valid number. # Application specific messages -messages.trash.confirmation=This will empty trash and delete messages permanently. Do you want to continue? -default.created.poll=The poll has been created! +messages.trash.confirmation=This action will delete your messages permanently. Do you want to continue? +default.created.poll=Your poll has been created! default.search.label=Clear search default.search.betweendates.title=Between dates: default.search.moresearchoption.label=More search options -default.search.date.format=d/M/yyyy +default.search.date.format=d/M/yyyy default.search.moreoption.label=More options - # SMSLib Fconnection -smslibfconnection.label=Phone/Modem -smslibfconnection.type.label=Type -smslibfconnection.name.label=Name -smslibfconnection.port.label=Port -smslibfconnection.baud.label=Baud rate -smslibfconnection.pin.label=PIN -smslibfconnection.imsi.label=SIM IMSI -smslibfconnection.serial.label=Device Serial # -smslibfconnection.send.label=Use Modem for sending Messages -smslibfconnection.receive.label=Use Modem for Receiving Messages -smslibFconnection.send.validator.error.send=Modem should be used for sending -smslibFconnection.receive.validator.error.receive=or receiving messages -smslibfconnection.description=Connect to USB, serial and bluetooth modems or phones -smslibfconnection.global.info=FrontlineSMS will attempt to automatically configure any connected modem or phone, but you can manually configure them here - +smslib.label=Phone/Modem +smslib.type.label=Type +smslib.name.label=Name +smslib.manufacturer.label=Manufacturer +smslib.model.label=Model +smslib.port.label=Port +smslib.baud.label=Baud rate +smslib.pin.label=PIN +smslib.imsi.label=SIM IMSI +smslib.serial.label=Device Serial # +smslib.sendEnabled.label=Use for sending +smslib.receiveEnabled.label=Use for receiving +smslibFconnection.sendEnabled.validator.error.send=Modem should be used for sending +smslibFconnection.receiveEnabled.validator.error.receive=or receiving messages +smslib.description=Connect to USB, serial and bluetooth modems or phones +smslib.global.info=FrontlineSMS will attempt to automatically configure any connected modem or phone, but you can manually configure them here # Email Fconnection -emailfconnection.label=Email -emailfconnection.type.label=Type -emailfconnection.name.label=Name -emailfconnection.receiveProtocol.label=Protocol -emailfconnection.serverName.label=Server Name -emailfconnection.serverPort.label=Server Port -emailfconnection.username.label=Username -emailfconnection.password.label=Password - +email.label=Email +email.type.label=Type +email.name.label=Name +email.receiveProtocol.label=Protocol +email.serverName.label=Server Name +email.serverPort.label=Server Port +email.username.label=Username +email.password.label=Password # CLickatell Fconnection -clickatellfconnection.label=Clickatell Account -clickatellfconnection.type.label=Type -clickatellfconnection.name.label=Name -clickatellfconnection.apiId.label=API ID -clickatellfconnection.username.label=Username -clickatellfconnection.password.label=Password -clickatellfconnection.description=Send an receive messages through a Clickatell account -clickatellfconnection.global.info=You will need to configure an account with Clickatell (www.clickatell.com). - +clickatell.label=Clickatell +clickatell.type.label=Type +clickatell.name.label=Name +clickatell.apiId.label=API ID +clickatell.username.label=Username +clickatell.password.label=Password +clickatell.sendToUsa.label=Send to United States +clickatell.fromNumber.label=From number +clickatell.description=Send and receive messages through a Clickatell account. +clickatell.global.info=You will need to configure an account with Clickatell (www.clickatell.com). +clickatellFconnection.fromNumber.validator.invalid=A 'From Number' is required for sending messages to the United States. +# TODO: Change markup below to markdown +clickatell.info-local=In order to set up a Clickatell connection, you must first have a Clickatell account. If you do not have one, please go to the Clickatell site and register for a 'Developer's Central Account'. It is free to sign up for test messages, and the process should take less that 5 minutes.

Once you have an active Clickatell account, you will need to 'Create a Connection (API ID)' from the front page. First, select 'APIs,' then select 'Set up a new API.' From there, choose 'add HTTP API' with the default settings, then enter the relevant details below.

The 'Name' field is just for your own reference for your Frontline account, and not related to the Clickatell API, e.g. 'My local message connection'. +clickatell.info-clickatell=The following details should be copied and pasted directly from the Clickatell HTTP API screen. +#Nexmo Fconnection +nexmo.label=Nexmo +nexmo.type.label=Nexmo connection +nexmo.name.label=Name +nexmo.api_key.label=API key +nexmo.api_secret.label=API secret +nexmo.fromNumber.label=From number +nexmo.description=Send and receive messages through a Nexmo account. +nexmo.receiveEnabled.label=Receiving enabled +nexmo.sendEnabled.label=Sending enabled +# FrontlineSync Fconnection +frontlinesync.label=FrontlineSync Beta +frontlinesync.description=Use an Android phone with Frontline's native app to send and receive SMS and capture missed calls. +frontlinesync.info-setup.p1=FrontlineSync is an Android app that allows you to use your Android device as a gateway for FrontlineSMS and FrontlineCloud. Using FrontlineSync, you can send and receive SMS, and also track missed calls. +frontlinesync.info-setup.p2=To set up a FrontlineSync connection, install the app on your phone from the Google Play link below, give the connection a name, and click next. Once you submit this form, you will be provided information that you need to enter into the Android app. +frontlinesync.passcode-setup=The FrontlineSync app on your Android device will ask you to enter the passcode below the first time you open the app. You can also reconfigure FrontlineSync by navigating to the Configure Screen via the Android app's main menu. +frontlinesync.final-setup=Click Next to save this configuration. +frontlinesync.secret.label=Passcode +frontlinesync.type.label=Type +frontlinesync.name.label=Name +frontlinesync.info-name=Finally, you should name your FrontlineSync connection with a name of your choice, e.g. 'Charlie's work Android'. +frontlinesync.info-sendEnabled=FrontlineSync will always upload missed calls and incoming text messages from your connected Android. You can manage SMS sending below +frontlinesync.sendEnabled.label=Enable SMS sending +frontlinesync.api.title=Enter the following details in the FrontlineSync Android App +frontlinesync.api.url.label=FrontlineSMS URL +frontlinesync.api.connection.id.label=Android Identifier +frontlinesync.api.connection.secret.label=Passcode +frontlinesync.sendEnabled.sync.config.label=Use your Android to send SMS (forward SMS to FrontlineSync) +frontlinesync.receiveEnabled.sync.config.label=Receive SMS from your Android (upload SMS from FrontlineSync) +frontlinesync.missedCallEnabled.sync.config.label=Receive Missed Calls from your Android (upload Missed Calls from FrontlineSync) +frontlinesync.sync.config.button=Save changes and push to your Android +frontlinesync.connection.options.label=Connection options +frontlinesync.sync.config.dirty.false=have been synced with your Android +frontlinesync.sync.config.dirty.true=waiting to be synced with your Android +frontlinesync.checkInterval.label=Check for outgoing messages +frontlinesync.checkInterval.manual=Do not automatically check +frontlinesync.checkInterval.1=every minute +frontlinesync.checkInterval.5=every 5 minutes +frontlinesync.checkInterval.15=every 15 minutes +frontlinesync.checkInterval.30=every half hour +frontlinesync.checkInterval.60=every hour +frontlinesync.checkInterval.120=every 2 hours # Smssync Fconnection -smssyncfconnection.label=SMSSync -smssyncfconnection.name.label=Name -smssyncfconnection.type.label=Type -smssyncfconnection.receiveEnabled.label=Receive enabled -smssyncfconnection.sendEnabled.label=Send enabled -smssyncfconnection.secret.label=Secret -smssyncfconnection.description=Use an Android phone with the Smssync app installed to send and receive SMS with FrontlineSMS -smssyncfconnection.field.secret.info=On your app, set the secret to match this field -smssyncfconnection.global.info=Download the SMSSync app from smssync.ushahidi.com - +smssync.label=SMSSync +smssync.name.label=Name +smssync.type.label=Type +smssync.receiveEnabled.label=Receive enabled +smssync.sendEnabled.label=Send enabled +smssync.secret.label=Secret +smssync.timeout.label=Timeout (mins) +smssync.description=Use an Android phone with the SMSSync app installed to send and receive SMS using Frontline products. +smssync.field.secret.info=On your app, set the secret to match this field. +smssync.global.info=Download and install [SMSSync from the Android App store](https://play.google.com/store/apps/details?id=org.addhen.smssync&hl=en) to your Android phone. +smssync.timeout=The Android phone associated with "{0}" has not contacted your Frontline account for {1} minute(s) [edit] +smssync.info-setup=Frontline products enable you to send and receive messages through your Android phone. In order to do this you will need to:\n\n1. Input a 'Secret' and name your connection. A secret is simply a password of your choice.\n2. Download and install [SMSSync from the Android App store](https://play.google.com/store/apps/details?id=org.addhen.smssync&hl=en) to your Android phone\n3. Once you have created this connection, you can create a new Sync URL within SMSSync on your Android phone by entering the connection URL (generated by your Frontline product and displayed on the next page) and your chosen secret. See [The SMSSync Site](http://smssync.ushahidi.com/howto) for more help. +smssync.info-timeout=If SMSSync does not contact your Frontline product for a certain duration (default 60 minutes), your queued messages will NOT be sent, and you will see a notification that the messages failed to send. Select this duration below: +smssync.info-name=Finally, you should name your SMSSync connection with a name of your choice, e.g. 'Bob's work Android'. +smssync.lastConnected.time=Last seen: {0} +smssync.lastConnected.never=Has not contacted Frontline yet # Messages Tab message.create.prompt=Enter message -message.character.count=Characters remaining {0} ({1} SMS message(s)) -message.character.count.warning=May be longer after performing substitutions +message.character.count=Characters remaining: {0} ({1} SMS message(s)) +message.character.count.warning=May be longer after performing substitutions. +message.header.inbox=Inbox +message.header.sent=Sent +message.header.pending=Pending +message.header.trash=Trash +message.header.folder=Folders +message.header.activityList=ActivityList +message.header.folderList=FolderList announcement.label=Announcement -announcement.description=Send an announcement message and organise the responses -announcement.info1=The announcement has been saved and the messages have been added to the pending message queue. -announcement.info2=It may take some time for all the messages to be sent, depending on the number of messages and the network connection. -announcement.info3=To see the status of your messages, open the 'Pending' messages folder. -announcement.info4=To see the announcement click on it in the left hand menu. +announcement.description=Send an announcement to your contacts and organize the responses +announcement.info1=The announcement has been saved. The messages have been added to the pending message queue. +announcement.info2=It may take some time for all of your messages to be sent, depending on the number of messages and the network connection. +announcement.info3=To see the status of your messages, go to 'Pending' messages. +announcement.info4=To see the announcement, click on it in the left-hand menu. announcement.validation.prompt=Please fill in all required fields announcement.select.recipients=Select recipients announcement.confirm=Confirm @@ -242,8 +299,6 @@ announcement.moreactions.rename=Rename announcement announcement.moreactions.edit=Edit announcement announcement.moreactions.export=Export announcement frontlinesms2.Announcement.name.unique.error.frontlinesms2.Announcement.name={1} {0} "{2}" must be unique - - archive.inbox=Inbox archive archive.sent=Sent archive archive.activity=Activity archive @@ -257,35 +312,36 @@ archive.activity.type=Type archive.activity.date=Date archive.activity.messages=Messages archive.activity.list.none=  No archived activities - +archive.header=Archive autoreply.enter.keyword=Enter keyword -autoreply.create.message=Enter message +autoreply.create.message=Enter an auto reply message +activity.autoreply.sort.description=Anyone sending you a message beginning with your chosen keyword will receive your Autoreply. Messages not beginning with the keyword will go to your inbox. +activity.autoreply.disable.sorting.description=Messages from your contacts will go into your inbox and will not receive an automatic response. You will need to manually move messages into this activity in order for contacts to receive the Autoreply. autoreply.confirm=Confirm autoreply.name.label=Message autoreply.details.label=Confirm details autoreply.label=Autoreply autoreply.keyword.label=Keyword(s) -autoreply.description=Automatically respond to incoming messages -autoreply.info=The autoreply has been created, any messages containing your keyword will be added to this Autoreply activity which can be viewed by clicking on it in the right hand menu. -autoreply.info.warning=Autoreplies without a keyword will be sent to all incoming messages -autoreply.info.note=Note: If you archive the Autoreply, incoming messages will no longer be sorted for it. +autoreply.description=Automatically respond to incoming messages. +autoreply.info=Your auto reply activity has been created, and any messages containing your keyword will be added to this auto reply, which can be viewed by clicking on it in the right-hand menu. +autoreply.info.warning=Without a keyword, your auto reply will be sent to all incoming messages. +autoreply.info.note=Note: If you archive the auto reply, incoming messages will no longer be sorted into this activity. autoreply.validation.prompt=Please fill in all required fields -autoreply.message.title=Message to be sent back for this autoreply: +autoreply.message.title=Message to be sent back for this auto reply: autoreply.keyword.title=Sort messages automatically using a keyword: -autoreply.name.prompt=Name this autoreply +autoreply.name.prompt=Name this autoreply activity autoreply.message.count=0 characters (1 SMS message) -autoreply.moreactions.delete=Delete autoreply -autoreply.moreactions.rename=Rename autoreply -autoreply.moreactions.edit=Edit autoreply -autoreply.moreactions.export=Export autoreply -autoreply.all.messages=Do not use keyword (All incoming messages will receive this autoreply) +autoreply.moreactions.delete=Delete auto reply activity +autoreply.moreactions.rename=Rename auto reply activity +autoreply.moreactions.edit=Edit auto reply activity +autoreply.moreactions.export=Export auto reply activity +autoreply.all.messages=Do not use keyword (All incoming messages will receive this autoreply) autoreply.text.none=None frontlinesms2.Autoreply.name.unique.error.frontlinesms2.Autoreply.name={1} {0} "{2}" must be unique frontlinesms2.Autoreply.name.validator.error.frontlinesms2.Autoreply.name=Autoreply name must be unique frontlinesms2.Keyword.value.validator.error.frontlinesms2.Autoreply.keyword.value=Keyword "{2}" is already in use frontlinesms2.Autoreply.autoreplyText.nullable.error.frontlinesms2.Autoreply.autoreplyText=Message cannot be blank - -autoforward.title={0} autoforward +autoforward.title={0} autoforward.label=Autoforward autoforward.description=Automatically forward incoming messages to contacts autoforward.recipientcount.current=Currently {0} recipients @@ -298,21 +354,24 @@ autoforward.keyword.label=Keyword(s) autoforward.name.label=Message autoforward.contacts=Contacts autoforward.groups=Groups -autoforward.info=The autoforward has been created, any messages containing your keyword will be added to this Autoforward activity which can be viewed by clicking on it in the right hand menu. -autoforward.info.warning=An autoforward without a keyword will result in all incoming messages being forwarded -autoforward.info.note=Note: If you archive the Autoforward, incoming messages will no longer be sorted for it. -autoforward.save=The Autoforward has been saved! -autoforward.saved=The Autoforward has been saved! +autoforward.info=Your auto forward activity has been created, and any messages containing your keyword will be added to this activity, which can be viewed by clicking on it in the right-hand menu. +autoforward.info.warning=Without a keyword, all incoming messages being forwarded. +autoforward.info.note=Note: If you archive this, incoming messages will no longer be sorted into this activity. +autoforward.save=The auto forward activity has been saved! +autoforward.save.success={0} Autoforward has been saved! autoforward.global.keyword=None (all incoming messages will be processed) autoforward.disabled.keyword=None (automatic sorting disabled) autoforward.keyword.none.generic=None autoforward.groups.none=None autoforward.contacts.none=None autoforward.message.format=Message - contact.new=New Contact -contact.list.no.contact=No contacts here! +contact.import.label=Import Contacts +contact.export.label=Export Contacts +contact.list.no.contact=No contacts have been added. +contact.list.no.selected=No contact selected. contact.header=Contacts +contact.header.group=Contacts >> {0} contact.all.contacts=All contacts ({0}) contact.create=Create new contact contact.groups.header=Groups @@ -325,65 +384,100 @@ contact.customfield.addmoreinformation=Add more information... contact.customfield.option.createnew=Create new... contact.name.label=Name contact.phonenumber.label=Mobile -contact.phonenumber.international.warning=This number is not in international format. This may cause problems matching messages to contacts. +contact.phonenumber.l10nWarning.header=It looks like this number is in local format. +contact.phonenumber.l10nWarning.description=This may not work if you are using internet aggregators. Adding a '+' and the country code will ensure the number works for all connection types. +contact.phonenumber.l10nWarning.dismiss=Don't show me this message again +contact.phonenumber.NonNumericNotAllowedWarning.description=Non-numeric characters have been detected in your number. These are removed when saving +contact.phonenumber.NonNumericNotAllowedWarning.dismiss=Don't show me this message again contact.notes.label=Notes contact.email.label=Email contact.groups.label=Groups contact.notinanygroup.label=Not part of any Groups contact.messages.label=Messages -contact.sent.messages={0} messages sent +contact.messages.sent={0} messages sent contact.received.messages={0} messages received contact.search.messages=Search for messages contact.select.all=Select All - +contact.search.placeholder=Search your contacts, or enter phone numbers +contact.send.message=Send {0} a message +contact.search.helptext=Search below for your contacts and groups, or enter phone numbers directly +contact.search.contact=Contacts +contact.search.smartgroup=Smart Groups +contact.search.group=Groups +contact.search.address=Add phone number +contact.not.found=Contact not found +contact.action.delete=Delete this Contact +contact.field.name.placeholder=John Smith +contact.field.mobile.placeholder=+1213231454422 +contact.field.email.placeholder=john@example.com +contact.field.click.to.edit=Click to edit +contact.mobile.unique=Number already assigned to a contact +contact.name.validator.invalid=Name or number required +contact.mobile.validator.invalid=Name or number required +group.not.found=Group not found +smartgroup.not.found=Smart Group not found group.rename=Rename group group.edit=Edit group group.delete=Delete group group.moreactions=More actions... - +group.export.label=Export {0} customfield.validation.prompt=Please fill in a name customfield.validation.error=Name already exists customfield.name.label=Name -export.contact.info=To export contacts from FrontlineSMS, choose the type of export and the information to be included in the exported data. -export.message.info=To export messages from FrontlineSMS, choose the type of export and the information to be included in the exported data. +export.contact.info=To export contacts from your Frontline account, choose the type of export and the information to be included in the exported data. +export.message.info=To export messages from your Frontline account, choose the type of export and the information to be included in the exported data. export.selectformat=Select an output format export.csv=CSV format for use in spreadsheet export.pdf=PDF format for printing +export.vcf=VCard format for use in contact management software folder.name.label=Name -group.delete.prompt=Are you sure you want to delete {0}? WARNING: This cannot be undone +group.delete.prompt=Are you sure you want to delete {0}? WARNING: This cannot be undone. layout.settings.header=Settings activities.header=Activities activities.create=Create new activity folder.header=Folders folder.create=Create new folder -folder.label=folder +folder.label=Folder message.folder.header={0} Folder fmessage.trash.actions=Trash actions... fmessage.trash.empty=Empty trash fmessage.to.label=To -trash.empty.prompt=All messages and activities in the trash will be deleted permanently +trash.empty.prompt=All messages and activities in the trash will be deleted permanently. fmessage.responses.total={0} responses total fmessage.label=Message fmessage.label.multiple={0} messages poll.prompt=Name this poll poll.details.label=Confirm details poll.message.label=Message -poll.choice.validation.error.deleting.response=A saved choice cannot have an empty value +poll.choice.validation.error.deleting.response=A saved choice cannot have an empty value. poll.alias=Aliases poll.keywords=Keywords -poll.aliases.prompt=Enter the aliases for the corresponding options. -poll.aliases.prompt.details=You can enter multiple keywords for each option, separated with commas. The first keyword will be sent in the poll instructions message. +poll.aliases.prompt=Enter any aliases for the corresponding options. +poll.keywords.prompt.details=The top-level keyword will name the poll and be sent in the poll instructions message. Each response can also have alternative short cut keywords. +poll.keywords.prompt.more.details=You may enter multiple keywords separated by commas for the top-level and responses. If no top-level keywords are entered below, then these response keywords need to be unique across all activities. +poll.keywords.response.label=Response Keywords +poll.response.keyword=Set response keywords +poll.set.keyword=Set a top-level keyword poll.keywords.validation.error=Keywords should be unique poll.sort.label=Auto-sort poll.autosort.no.description=Don't sort responses automatically. -poll.autosort.description=Sort responses automatically. Sort messages with the keyword (optional) +poll.autosort.description=Sort responses automatically. poll.sort.keyword=keyword -poll.sort.by=Sort by +poll.sort.toplevel.keyword.label=Top-level keyword(s) (optional) +poll.sort.by=Sort by poll.autoreply.label=Auto-reply poll.autoreply.none=none poll.recipients.label=Recipients poll.recipients.none=None poll.toplevelkeyword=Top-level keywords +poll.sort.example.toplevel=e.g TEAM +poll.sort.example.keywords.A=e.g A, AMAZING +poll.sort.example.keywords.B=e.g B, BEAUTIFUL +poll.sort.example.keywords.C=e.g C, COURAGEOUS +poll.sort.example.keywords.D=e.g D, DELIGHTFUL +poll.sort.example.keywords.E=e.g E, EXEMPLARY +poll.sort.example.keywords.yn.A=e.g YES, YAP +poll.sort.example.keywords.yn.B=e.g No, NOP #TODO embed javascript values poll.recipients.count=contacts selected poll.messages.count=messages will be sent @@ -395,34 +489,28 @@ poll.messages.sent={0} messages sent poll.response.enabled=Auto Response Enabled poll.message.edit=Edit message to be sent to recipients poll.message.prompt=The following message will be sent to the recipients of the poll -poll.message.count=Characters remaining 160 (1 SMS message) - +poll.message.count=Characters remaining: 160 (1 SMS message) poll.moreactions.delete=Delete poll poll.moreactions.rename=Rename poll poll.moreactions.edit=Edit poll poll.moreactions.export=Export poll - folder.moreactions.delete=Delete folder folder.moreactions.rename=Rename folder folder.moreactions.export=Export folder - - #TODO embed javascript values -poll.reply.text=Reply "{0} {1}" for Yes, "{2} {3}" for No. -poll.reply.text1={0} "{1} {2}" for {3} +poll.reply.text=Reply "{0}" for Yes, "{1}" for No. +poll.reply.text1={0} "{1}" for {2} poll.reply.text2=Please answer 'Yes' or 'No' -poll.reply.text3= or -poll.reply.text4={0} {1} +poll.reply.text3=or poll.reply.text5=Reply poll.reply.text6=Please answer poll.message.send={0} {1} poll.recipients.validation.error=Select contacts to send messages to -frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name = {1} {0} "{2}" must be unique +frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name={1} {0} "{2}" must be unique frontlinesms2.Poll.responses.validator.error.frontlinesms2.Poll.responses=Response options can not be identical -frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value = Keyword "{2}" is already in use - -wizard.title.new=New -wizard.fmessage.edit.title=Edit {0} +frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value=Keyword "{2}" is already in use +wizard.title.new=New +wizard.fmessage.edit.title=Edit {0} popup.title.saved={0} saved! popup.activity.create=Create New Activity : Select type popup.smartgroup.create=Create smart group @@ -441,27 +529,27 @@ smallpopup.messages.export.title=Export Results ({0} messages) smallpopup.test.message.title=Test message smallpopup.recipients.title=Recipients smallpopup.folder.title=Folder -smallpopup.group.title=Group +smallpopup.folder.create.title=Create Folder +smallpopup.group.create.title=Group smallpopup.contact.export.title=Export smallpopup.contact.delete.title=Delete contact.selected.many={0} contacts selected group.join.reply.message=Welcome group.leave.reply.message=Bye fmessage.new.info=You have {0} new messages. Click to view -wizard.quickmessage.title=Quick Message +wizard.quickmessage.title=Send Message wizard.messages.replyall.title=Reply All wizard.send.message.title=Send Message wizard.ok=Ok wizard.create=Create +wizard.save=Save wizard.send=Send common.settings=Settings common.help=Help - validation.nospaces.error=Keyword should not have spaces activity.validation.prompt=Please fill in all required fields -autoreply.blank.keyword=Blank keyword. A response will be sent to all incoming messages - - +validator.invalid.name=Another activity exists with the name {2} +autoreply.blank.keyword=Note: blank keyword. A response will be sent to all incoming messages. poll.type.prompt=Select the kind of poll to create poll.question.yes.no=Question with a 'Yes' or 'No' answer poll.question.multiple=Multiple choice question (e.g. 'Red', 'Blue', 'Green') @@ -472,9 +560,10 @@ poll.replies.description=When an incoming message is identified as a poll respon poll.autoreply.send=Send an automatic reply to poll responses poll.responses.prompt=Enter possible responses (between 2 and 5) poll.sort.header=Sort messages automatically using a keyword (optional) -poll.sort.description=If people send in poll responses using a keyword, FrontlineSMS can automatically sort the messages on your system. +poll.sort.enter.keywords=Enter keywords for the poll and the responses +poll.sort.description=If users send poll responses with a specific keyword, then Frontline products can automatically sort the messages into this activity. poll.no.automatic.sort=Do not sort messages automatically -poll.sort.automatically=Sort messages automatically that have the following keyword +poll.sort.automatically=Sort messages automatically that have the following keyword: poll.validation.prompt=Please fill in all required fields poll.name.validator.error.name=Poll names must be unique pollResponse.value.blank.value=Poll response value can't be blank @@ -487,16 +576,19 @@ poll.edit.message=Edit Message poll.recipients=Select recipients poll.confirm=Confirm poll.save=The poll has been saved! +poll.save.success={0} Poll has been saved! poll.messages.queue=If you chose to send a message with this poll, the messages have been added to the pending message queue. poll.messages.queue.status=It may take some time for all the messages to be sent, depending on the number of messages and the network connection. poll.pending.messages=To see the status of your message, open the 'Pending' messages folder. poll.send.messages.none=No messages will be sent quickmessage.details.label=Confirm details quickmessage.message.label=Message +quickmessage.instructions=Enter the message you would like to send, select the recipients and send! +quickmessage.recipients.instructions=Enter the text you would like to send* quickmessage.message.none=None quickmessage.recipient.label=Recipient quickmessage.recipients.label=Recipients -quickmessage.message.count=Characters remaining 160 (1 SMS message) +quickmessage.message.count=Characters remaining: 160 (1 SMS message) quickmessage.enter.message=Enter message quickmessage.select.recipients=Select recipients quickmessage.confirm=Confirm @@ -509,8 +601,7 @@ quickmessage.phonenumber.label=Add phone number: quickmessage.phonenumber.add=Add quickmessage.selected.recipients=recipients selected quickmessage.validation.prompt=Please fill in all required fields - -fmessage.number.error=Characters in this field will be removed when saved +fmessage.number.error=Non-numeric characters in this field will be removed when saved search.filter.label=Limit Search to search.filter.group=Select group search.filter.activities=Select activity/folder @@ -520,7 +611,7 @@ search.filter.sent=Only sent messages search.filter.archive=Include Archive search.betweendates.label=Between dates search.header=Search -search.quickmessage=Quick message +search.quickmessage=Send message search.export=Export results search.keyword.label=Keyword search.contact.name.label=Contact name @@ -528,32 +619,39 @@ search.contact.name=Contact name search.result.header=Results search.moreoptions.label=More options settings.general=General -settings.connections=Phones & connections -settings.logs=System +settings.porting=Import and Export +settings.import.contact.review.page.header=Review Import +settings.import.contact.review.header=Review your contact import +settings.import.contact.review.recognisedTitles=In order to match your names and numbers with the built-in Frontline contact fields you will need to match your column headings exactly:
{0}. +settings.import.contact.review.unrecognisedTitles=The column heading will turn green once it matches. Ensure all characters are exact including the case. Columns headings that are yellow will be imported as custom fields. Any empty column headings, shown in red, will cause the import to fail. +settings.import.contact.review.submit=Import all contacts +settings.import.contact.review.csv.limit.reached=CSV import is currently limited to {0} contacts. The list below shows the first {0} rows of your CSV import. If you need to import more than this number of contacts, please consider splitting your CSV file or using a VCF file. See our knowledge base for more information +settings.connections=Phones and connections +settings.status=Usage Statistics +settings.logs=System Logs settings.general.header=Settings > General -settings.logs.header=System Logs +settings.porting.header=Settings > Import & Export +settings.logs.header=Settings > System Logs logs.none=You have no logs. logs.content=Message logs.date=Time logs.filter.label=Show logs for logs.filter.anytime=all time -logs.filter.1day=last 24 hours -logs.filter.3days=last 3 days -logs.filter.7days=last 7 days -logs.filter.14days=last 14 days -logs.filter.28days=last 28 days +logs.filter.days.1=last 24 hours +logs.filter.days.3=last 3 days +logs.filter.days.7=last 7 days +logs.filter.days.14=last 14 days +logs.filter.days.28=last 28 days logs.download.label=Download system logs logs.download.buttontext=Download Logs logs.download.title=Download logs to send logs.download.continue=Continue - -smartgroup.validation.prompt=Please fill in all the required fields. You may only specify one rule per field. -smartgroup.info=To create a Smart group, select the criteria you need to be matched for contacts for this group +smartgroup.validation.prompt=Please fill in all the required fields. You may only specify one rule per field. +smartgroup.info=To create a Smart Group, select the criteria you need to be matched for contacts for this group. smartgroup.contains.label=contains smartgroup.startswith.label=starts with smartgroup.add.anotherrule=Add another rule smartgroup.name.label=Name - modem.port=Port modem.description=Description modem.locked=Locked? @@ -567,52 +665,51 @@ traffic.all.folders.activities=Show all activities/folders traffic.sent=Sent traffic.received=Received traffic.total=Total - tab.message=Messages tab.archive=Archive tab.contact=Contacts tab.status=Status +tab.connection=Connections tab.search=Search - -help.info=This version is a beta so there is no built-in help. Please go to the user forums to get help at this stage - +help.info=This version is a beta release, so there are no built-in help files. Please go to the user forums online to get help at this stage +help.notfound=This help file is not yet available, sorry. # IntelliSms Fconnection -intellismsfconnection.label=IntelliSms Account -intellismsfconnection.type.label=Type -intellismsfconnection.name.label=Name -intellismsfconnection.username.label=Username -intellismsfconnection.password.label=Password - -intellismsfconnection.send.label=Use for sending -intellismsfconnection.receive.label=Use for receiving -intellismsfconnection.receiveProtocol.label=Protocol -intellismsfconnection.serverName.label=Server Name -intellismsfconnection.serverPort.label=Server Port -intellismsfconnection.emailUserName.label=Username -intellismsfconnection.emailPassword.label=Password -intellismsfconnection.description=Send an receive messages through an Intellisms account -intellismsfconnection.global.info=You will need to configure an account with Intellisms (www.intellisms.co.uk). - +intellisms.label=IntelliSMS +intellisms.type.label=Type +intellisms.name.label=Name +intellisms.username.label=Username +intellisms.password.label=Password +intellisms.sendEnabled.label=Use for sending +intellisms.receiveEnabled.label=Use for receiving +intellisms.receiveProtocol.label=Protocol +intellisms.serverName.label=Server Name +intellisms.serverPort.label=Server Port +intellisms.emailUserName.label=Username +intellisms.emailPassword.label=Password +intellisms.description=Send and receive messages through an IntelliSMS connection. +intellisms.global.info=You will need to configure an account with IntelliSMS (www.intellisms.co.uk). +intelliSmsFconnection.send.validator.invalid=You cannot configure a connection without SEND or RECEIVE functionality. +intelliSmsFconnection.receive.validator.invalid=You cannot configure a connection without SEND or RECEIVE functionality. #Controllers contact.label=Contact(s) -contact.edited.by.another.user=Another user has updated this Contact while you were editing -contact.exists.prompt=There is already a contact with that number -contact.exists.warn=Contact with this number already exists +contact.edited.by.another.user=Another user has updated this Contact while you were editing. +contact.exists.prompt=There is already a contact with that number. +contact.exists.warn=A contact with this number already exists. contact.view.duplicate=View duplicate -contact.addtogroup.error=Cannot add and remove from the same group! +contact.addtogroup.error=You cannot add and remove from the same group. contact.mobile.label=Mobile -contact.email.label=Email fconnection.label=Fconnection fconnection.name=Fconnection -fconnection.unknown.type=Unknown connection type: +fconnection.unknown.type=Unknown connection type: fconnection.test.message.sent=Test message queued for sending! -announcement.saved=Announcement has been saved and message(s) have been queued to send -announcement.not.saved=Announcement could not be saved! +announcement.saved=Your Announcement has been saved and message(s) have been queued to send. +announcement.not.saved=Your Announcement could not be saved. +announcement.save.success={0} Announcement has been saved! announcement.id.exist.not=Could not find announcement with id {0} -autoreply.saved=Autoreply has been saved! -autoreply.not.saved=Autoreply could not be saved! +autoreply.save.success={0} Autoreply has been saved! +autoreply.not.saved=Your auto reply activity could not be saved. report.creation.error=Error creating report -export.message.title=FrontlineSMS Message Export +export.message.title=Message Export export.database.id=DatabaseID export.message.date.created=Date Created export.message.text=Text @@ -620,7 +717,7 @@ export.message.destination.name=Destination Name export.message.destination.mobile=Destination Mobile export.message.source.name=Source Name export.message.source.mobile=Source Mobile -export.contact.title=FrontlineSMS Contact Export +export.contact.title=Contact Export export.contact.name=Name export.contact.mobile=Mobile export.contact.email=Email @@ -631,72 +728,76 @@ export.messages.name2={0} ({1} messages) export.contacts.name1={0} group ({1} contacts) export.contacts.name2={0} smart group ({1} contacts) export.contacts.name3=All contacts ({0} contacts) -folder.label=Folder folder.archived.successfully=Folder was archived successfully! folder.unarchived.successfully=Folder was unarchived successfully! folder.trashed=Folder has been trashed! folder.restored=Folder has been restored! folder.exist.not=Could not find folder with id {0} folder.renamed=Folder Renamed - group.label=Group group.name.label=Name -group.update.success=Group updated successfully -group.save.fail=Group save failed -group.delete.fail=Unable to delete group. In use by a subscription - -import.label=Import -import.backup.label=Import data from a previous backup -import.prompt.type=Select type of data to import -import.contacts=Contact details -import.messages=Message details -import.version1.info=To import data from version 1, please export them in English -import.prompt=Select a data file to import +group.update.success=Group updated successfully. +group.save.fail=Group save failed. +group.delete.fail=Unable to delete group. In use by a subscription activity. +import.label=Import contacts and messages +import.backup.label=You can use the form below to import your contacts. You can also import messages that you have from a previous Frontline project. +import.prompt.type=Select the type of data you wish to import before uploading +import.contacts.csv=Contacts in CSV format +import.contacts.vcf=Contacts in vCard or VCF format +import.messages=Messages in Frontline's CSV format +import.contacts.info=Contacts can be imported in either CSV (a common file format for outputting spreadsheet files) or vCard/vcf (a file format used by common address books such as Outlook, Google & Apple Contacts) format. +import.prompt=Select the file containing your data to initiate the import import.upload.failed=File upload has failed for some reason. -import.contact.save.error=Encountered error saving contact -import.contact.complete={0} contacts were imported; {1} failed -import.contact.failed.download=Download failed contacts (CSV) +import.contact.save.error=An error was encountered whilst saving the contact. +import.contact.complete=Congratulations! {0} contacts were successfully imported. +import.contact.exist=The imported contacts already exist. +import.contact.failed.label=Failed contact imports +import.contact.failed.info={0} contact(s) successfully imported.
{1} contact(s) could not be imported.
{2} +import.download.failed.contacts=Download a file containing the failed contacts. import.message.save.error=Encountered error saving message import.message.complete={0} messages were imported; {1} failed - -many.selected = {0} {1}s selected - -flash.message.activity.found.not=Activity could not be found -flash.message.folder.found.not=Folder could not be found +download.label=Download failed contacts +importing.status.label=Your contacts are being imported. You will be notified when the import is completed +import.maxfilesize.exceeded=The file you uploaded is too large. The maximum allowed is {0}MB +import.contact.failed.invalid.vcf.file=Contact importing failed because of invalid VCF upload +export.label=Export data from your Frontline workspace +export.backup.label=You can export your Frontline data as VCF/VCard, CSV or PDF +export.prompt.type=Select which data you wish to export +export.allcontacts=All of your contacts +export.inboxmessages=Your Inbox messages +export.submit.label=Export and download data +many.selected={0} {1}s selected +flash.message.activity.found.not=Activity could not be found. +flash.message.folder.found.not=Folder could not be found. flash.message=Message flash.message.fmessage={0} message(s) flash.message.fmessages.many={0} SMS messages flash.message.fmessages.many.one=1 SMS message fmessage.exist.not=Could not find message with id {0} -flash.message.poll.queued=Poll has been saved and message(s) has been queued to send -flash.message.poll.saved=Poll has been saved +flash.message.poll.queued=Your Poll has been saved and messages are queued to send. flash.message.poll.not.saved=Poll could not be saved! system.notification.ok=OK system.notification.fail=FAIL -flash.smartgroup.delete.unable=Unable to delete smartgroup -flash.smartgroup.saved=Smart group {0} saved -flash.smartgroup.save.failed=Smart group save failed. Errors were {0} -smartgroup.id.exist.not=Could not find smartgroup with id {0} +flash.smartgroup.delete.unable=Unable to delete Smart Group +flash.smartgroup.saved=Smart Group {0} saved +flash.smartgroup.save.failed=Smart Group save failed. Errors were {0} +smartgroup.id.exist.not=Could not find smart group with id {0} smartgroup.save.failed=Failed to save smart group{0}with params {1}{2}errors: {3} -contact.name.label=Name -contact.phonenumber.label=Phone Number - searchdescriptor.searching=Searching -searchdescriptor.all.messages= all messages +searchdescriptor.all.messages=all messages searchdescriptor.archived.messages=, including archived messages searchdescriptor.exclude.archived.messages=, without archived messages searchdescriptor.only=, only {0} searchdescriptor.between=, between {0} and {1} searchdescriptor.from=, from {0} searchdescriptor.until=, until {0} -poll.title={0} poll -announcement.title={0} announcement -autoreply.title={0} autoreply -folder.title={0} folder +poll.title={0} +announcement.title={0} +autoreply.title={0} +folder.title={0} frontlinesms.welcome=Welcome to FrontlineSMS! \\o/ failed.pending.fmessages={0} pending message(s) failed. Go to pending messages section to view. - -subscription.title={0} subscription +subscription.title={0} subscription.info.group=Group: {0} subscription.info.groupMemberCount={0} members subscription.info.keyword=Top-level keywords: {0} @@ -705,40 +806,38 @@ subscription.info.joinKeywords=Join: {0} subscription.info.leaveKeywords=Leave: {0} subscription.group.goto=View Group subscription.group.required.error=Subscriptions must have a group - +subscription.save.success={0} Subscription has been saved! language.label=Language -language.prompt=Change the language of the FrontlineSMS user interface -frontlinesms.user.support=FrontlineSMS User Support -download.logs.info1=WARNING: The FrontlineSMS team are unable to respond directly to submitted logs. If you have a user support request, please check the Help files to see whether you can find the answer there. If not, report your issue via our user support forums: -download.logs.info2=Other users may even have reported the same problem and found a solution! To carry on and submit your logs, please click 'Continue' - +language.prompt=Change the language of the user interface. +frontlinesms.user.support=User Support +download.logs.info1=WARNING: The Frontline Team is unable to respond directly to submitted logs. If you have a user support request, please check the Help files to see whether you can find the answer there. If not, report your issue via our user support forums: +download.logs.info2=Other users may even have reported the same problem and found a solution! To carry on and submit your logs, please click 'Continue'. # Configuration location info configuration.location.title=Configuration Location -configuration.location.instructions=You can find your application configuration at {1}. These files include your database and other settings, which you may wish to back up elsewhere. - +configuration.location.description=These files include your database and other settings, which you may wish to back up elsewhere. +configuration.location.instructions=You can find your application configuration at: dynamicfield.contact_name.label=Contact Name dynamicfield.contact_number.label=Contact Number dynamicfield.keyword.label=Keyword dynamicfield.message_content.label=Message Content - -# Fmessage domain -fmessage.queued=Message has been queued to send to {0} -fmessage.queued.multiple=Message has been queued to send to {0} recipients -fmessage.retry.success=Message has been requeued to send to {0} -fmessage.retry.success.multiple={0} message(s) have been requeued for sending +# TextMessage domain +fmessage.queued=Message has been queued to send to {0}. +fmessage.queued.multiple=Message has been queued to send to {0} recipients. +fmessage.retry.success=Message has been re-queued to send to {0}. +fmessage.retry.success.multiple={0} message(s) have been re-queued for sending. fmessage.displayName.label=Name fmessage.text.label=Message fmessage.date.label=Date fmessage.to=To: {0} fmessage.to.multiple=To: {0} recipients -fmessage.quickmessage=Quick message +fmessage.quickmessage=Send message fmessage.archive=Archive fmessage.activity.archive=Archive {0} fmessage.unarchive=Unarchive {0} fmessage.export=Export fmessage.rename=Rename {0} fmessage.edit=Edit {0} -fmessage.delete=Delete {0} +fmessage.delete=Delete fmessage.moreactions=More actions... fmessage.footer.show=Show fmessage.footer.show.failed=Failed @@ -759,53 +858,46 @@ fmessage.resend=Resend fmessage.retry=Retry fmessage.reply=Reply fmessage.forward=Forward -fmessage.unarchive=Unarchive -fmessage.delete=Delete -fmessage.messages.none=No messages here! -fmessage.selected.none=No message selected +fmessage.messages.none=No messages have been received yet. +fmessage.messages.sent.none=No messages have been sent yet. +fmessage.selected.none=No message selected. fmessage.move.to.header=Move message to... fmessage.move.to.inbox=Inbox -fmessage.archive.many=Archive all +fmessage.archive.many=Archive selected fmessage.count=1 message fmessage.count.many={0} messages -fmessage.many= messages -fmessage.delete.many=Delete All -fmessage.reply.many=Reply all +fmessage.many=messages +fmessage.delete.many=Delete selected +fmessage.reply.many=Reply selected fmessage.restore=Restore fmessage.restore.many=Restore -fmessage.retry.many=Retry failed +fmessage.retry.many=Retry selected fmessage.selected.many={0} messages selected -fmessage.unarchive.many=Unarchive all - +fmessage.unarchive.many=Unarchive selected # TODO move to poll.* fmessage.showpolldetails=Show graph fmessage.hidepolldetails=Hide graph - # TODO move to search.* fmessage.search.none=No messages found fmessage.search.description=Start new search on the left - -fmessage.connection.receivedon=Received on: - +fmessage.connection.receivedon=Received on: {0} activity.name=Name activity.delete.prompt=Move {0} to trash. This will transfer all associated messages to the trash section. activity.label=Activity activity.categorize=Categorize response - magicwand.title=Add substitution expressions folder.create.success=Folder create successfully folder.create.failed=Could not create folder -folder.name.validator.error=Used folder name +folder.name.validator.error=Folder name already in use folder.name.blank.error=Folder name cannot be blank -poll.name.blank.error=Poll name cannot be blank -poll.name.validator.error=Used poll name -autoreply.name.blank.error=Autoreply name cannot be blank -autoreply.name.validator.error=Used autoreply name -announcement.name.blank.error=Announcement name cannot be blank -announcement.name.validator.error=Used announcement name +poll.name.blank.error=Activity name cannot be blank +poll.name.validator.error=Activity name already in use +autoreply.name.blank.error=Activity name cannot be blank +autoreply.name.validator.error=Activity name already in use +announcement.name.blank.error=Activity name cannot be blank +announcement.name.validator.error=Activity name already in use group.name.blank.error=Group name cannot be blank -group.name.validator.error=Used group name - +group.name.validator.error=Group name already in use #Jquery Validation messages jquery.validation.required=This field is required. jquery.validation.remote=Please fix this field. @@ -824,48 +916,46 @@ jquery.validation.rangelength=Please enter a value between {0} and {1} character jquery.validation.range=Please enter a value between {0} and {1}. jquery.validation.max=Please enter a value less than or equal to {0}. jquery.validation.min=Please enter a value greater than or equal to {0}. - # Webconnection common webconnection.select.type=Select Web Service or application to connect to: webconnection.type=Select Type -webconnection.title={0} web connection +webconnection.title={0} webconnection.label=Web Connection webconnection.description=Connect to web service. webconnection.sorting=Automatic sorting webconnection.configure=Configure service webconnection.api=Expose API -webconnection.api.info=FrontlineSMS can be configured to receive incoming requests from your remote service and trigger outgoing messages. For more details, see [this] +webconnection.api.info=FrontlineSMS can be configured to receive incoming requests from your remote service and trigger outgoing messages. For more details, see the Web Connection help section. webconnection.api.enable.label=Enable API webconnection.api.secret.label=API secret key: webconnection.api.disabled=API disabled webconnection.api.url=API URL - +webconnection.moreactions.retryFailed=retry failed uploads +webconnection.failed.retried=Failed web connections have been scheduled for resending. +webconnection.url.error.locahost.invalid.use.ip=Please use 127.0.0.1 instead of "locahost" for localhost urls +webconnection.url.error.url.start.with.http=Invalid URL (should start with http:// or https://) # Webconnection - generic webconnection.generic.label=Other web service webconnection.generic.description=Send messages to other web service webconnection.generic.subtitle=HTTP Web Connection - # Webconnection - Ushahidi/Crowdmap -webconnection.ushahidi.label=Crowdmap / Ushahidi -webconnection.ushahidi.description=Send messages to CrowdMap or to an Ushahidi server. -webconnection.ushahidi.key.description=The API key for either Crowdmap or Ushahidi can be found in the Settings on the Crowdmap or Ushahidi web site. -webconnection.ushahidi.url.label=Ushahidi deployment address: -webconnection.ushahidi.key.label=Ushahidi API Key: -webconnection.crowdmap.url.label=Crowdmap deployment address: -webconnection.crowdmap.key.label=Crowdmap API Key: +webconnection.ushahidi.label=Crowdmap Classic / Ushahidi +webconnection.ushahidi.description=Send messages to Crowdmap Classic or to an Ushahidi V2 server +webconnection.ushahidi.key.description=The API key for either Crowdmap Classic or Ushahidi V2 can be found in the Settings on the Crowdmap Classic or Ushahidi web site. +webconnection.ushahidi.url.label=Ushahidi V2 deployment address: +webconnection.ushahidi.key.label=Ushahidi V2 API Key: +webconnection.crowdmap.url.label=Crowdmap Classic deployment address: +webconnection.crowdmap.key.label=Crowdmap Classic API Key: webconnection.ushahidi.serviceType.label=Select Service -webconnection.ushahidi.serviceType.crowdmap=Crowdmap -webconnection.ushahidi.serviceType.ushahidi=Ushahidi +webconnection.ushahidi.serviceType.crowdmap=Crowdmap Classic +webconnection.ushahidi.serviceType.ushahidi=Ushahidi V2 webconnection.crowdmap.url.suffix.label=.crowdmap.com webconnection.ushahidi.subtitle=Web Connection to {0} webconnection.ushahidi.service.label=Service: -webconnection.ushahidi.url.label=Address: -webconnection.ushahidi.fsmskey.label=FrontlineSMS API Secret: -webconnection.ushahidi.crowdmapkey.label=Crowdmap/Ushahidi API Key: +webconnection.ushahidi.fsmskey.label=Frontline API Secret: +webconnection.ushahidi.crowdmapkey.label=Crowdmap Classic/Ushahidi API Key: webconnection.ushahidi.keyword.label=Keyword: -webconnection.api.disabled=API Disabled -frontlinesms2.GenericWebconnection.url.validator.error=Invalid URL (should start with http:// or https://) - +url.invalid.url=The URL provided is invalid. webconnection.confirm=Confirm webconnection.keyword.title=Transfer every message received containing the following keyword: webconnection.all.messages=Do not use keyword (All incoming messages will be forwarded to this web Connection @@ -878,32 +968,27 @@ webconnection.parameters=Configure information sent to the server webconnection.parameters.confirm=Configured information sent to the server webconnection.keyword.label=Keyword: webconnection.none.label=None -webconnection.url.label=Server Url: -webconnection.param.name=Name: -webconnection.param.value=Value: -webconnection.add.anotherparam=Add parameter - +webconnection.url.label=Enter your URL* +webconnection.param.name=Name* +webconnection.param.value=Value +webconnection.add.anotherparam=Add a new parameter to send to URL dynamicfield.message_body.label=Message Text dynamicfield.message_body_with_keyword.label=Message Text With keyword dynamicfield.message_src_number.label=Contact number dynamicfield.message_src_name.label=Contact name dynamicfield.message_timestamp.label=Message Timestamp - webconnection.keyword.validation.error=Keyword is required -webconnection.url.validation.error=Url is required - +webconnection.url.validation.error=URL is required webconnection.save=The Web Connection has been saved! webconnection.saved=Web Connection saved! - +webconnection.save.success={0} Web Connection has been saved! webconnection.generic.service.label=Service: -webconnection.generic.httpMethod.label=Http Method: +webconnection.generic.httpMethod.label=HTTP Method: webconnection.generic.url.label=Address: -webconnection.generic.keyword.label=Auto-sort: webconnection.generic.parameters.label=Configured information sent to the server: webconnection.generic.keyword.label=Keyword: webconnection.generic.key.label=API Key: frontlinesms2.Keyword.value.validator.error.frontlinesms2.UshahidiWebconnection.keyword.value=Invalid keyword Value - #Subscription i18n subscription.label=Subscription subscription.name.prompt=Name this Subscription @@ -913,17 +998,19 @@ subscription.select.group=Select the group for the subscription subscription.group.none.selected=Select group subscription.autoreplies=Autoreplies subscription.sorting=Automatic sorting +subscription.sorting.header=Process messages automatically using a keyword (optional) subscription.confirm=Confirm subscription.group.header=Select Group -subscription.group.description=Contacts can be added and removed from groups automatically when FrontlineSMS receives a message that includes a special keyword. -subscription.top.keyword.header=Enter top-level keywords (optional) -subscription.top.keyword.description=You can enter multiple keywords separated by commas. If you use this, a message will only be sorted into this subscription if the first word matches a keyword -subscription.keywords.header=Enter keywords for subscription actions -subscription.keywords.description=Enter keywords for joining and leaving the group. You can enter multiple keywords separated by commas. -subscription.default.action.header=Default action +subscription.group.description=Contacts can be added and removed from groups automatically when Frontline receives a message that includes a special keyword. +subscription.keyword.header=Enter keywords for this subscription +subscription.top.keyword.description=Enter the top-level keywords that will sort messages into this activity and apply the subscription action. +subscription.top.keyword.more.description=You may enter multiple top-level keywords for each option, separated with commas. Top-level keywords need to be unique across all activities. +subscription.keywords.header=Enter keywords for joining and leaving this group. +subscription.keywords.description=You may enter multiple keywords separated by commas. If no top-level keywords are entered above, then these join and leave keywords need to be unique across all activities. +subscription.default.action.header=Select an action when no keywords sent subscription.default.action.description=Select the desired action when a message matches the top-level keyword but none of the join or leave keywords: -subscription.keywords.leave=Keywords for leaving the group -subscription.keywords.join=Keywords for joining the group +subscription.keywords.leave=Leave keyword(s) +subscription.keywords.join=Join keyword(s) subscription.default.action.join=Add the contact to the group subscription.default.action.leave=Remove the contact from the group subscription.default.action.toggle=Toggle the contact's group membership @@ -946,39 +1033,40 @@ subscription.categorise.leave.label=Remove senders from {0} subscription.categorise.toggle.label=Toggle senders' membership of {0} subscription.join=Join subscription.leave=Leave - +subscription.sorting.example.toplevel=e.g SOLUTION +subscription.sorting.example.join=e.g SUBSCRIBE, JOIN +subscription.sorting.example.leave=e.g UNSUBSCRIBE, LEAVE subscription.keyword.required=Keyword is required subscription.jointext.required=Please enter join autoreply text subscription.leavetext.required=Please enter leave autoreply text - subscription.moreactions.delete=Delete subscription subscription.moreactions.rename=Rename subscription subscription.moreactions.edit=Edit subscription subscription.moreactions.export=Export subscription - # Generic activity sorting -activity.generic.sorting=Automatic sorting -activity.generic.sort.header=Automatic sorting -activity.generic.sort.description=FrontlineSMS can automatically move incoming messages into this activity. You can do this for all messages, or only those that match one of the specified keywords. -activity.generic.keywords.title=Enter keywords that messages must match. You can enter multiple keywords, separated by commas: +activity.generic.sorting=Automatic processing +activity.generic.sorting.subtitle=Process messages automatically using a keyword (optional) +activity.generic.sort.header=Process messages automatically using a keyword (optional) +activity.generic.sort.description=If people send in messages beginning with a particular keyword, Frontline products can automatically process the messages on your system. +activity.generic.keywords.title=Enter keywords for activity. You can enter multiple keywords separated by commas: +activity.generic.keywords.subtitle=Enter keywords for the activity +activity.generic.keywords.info=You can enter multiple keywords separated by commas: activity.generic.no.keywords.title=Do not use a keyword activity.generic.no.keywords.description=All incoming messages that do not match any other keywords will trigger this activity activity.generic.disable.sorting=Do not automatically sort messages -activity.generic.disable.sorting.description=Messages will not be automatically moved into this activity -activity.generic.enable.sorting=Sort incoming messages that match a keyword +activity.generic.disable.sorting.description=Messages will not be automatically processed by this activity +activity.generic.enable.sorting=Process responses containing a keyword automatically activity.generic.sort.validation.unique.error=Keywords must be unique activity.generic.keyword.in.use=The keyword {0} is already in use by activity {1} -activity.generic.global.keyword.in.use=Activity {0} is set to receive all messages that do not match other keywords. You can only have one active activity with this setting - +activity.generic.global.keyword.in.use=Activity {0} is set to receive all messages that do not match other keywords. You can only have one activity at a time with this setting #basic authentication -basic.authentication=Basic Authentication -basic.authentication.label=Require a username and password for accessing FrontlineSMS across the network -basic.authentication.enable=Enable Basic Authentication -basic.authentication.username=Username -basic.authentication.password=Password -basic.authentication.confirm.password=Confirm Password -basic.authentication.password.mismatch=Passwords don't match - +auth.basic.label=Basic Authentication +auth.basic.info=Require a username and password for accessing FrontlineSMS across the network +auth.basic.enabled.label=Enable Basic Authentication +auth.basic.username.label=Username +auth.basic.password.label=Password +auth.basic.confirmPassword.label=Confirm Password +auth.basic.password.mismatch=Passwords don't match newfeatures.popup.title=New Features newfeatures.popup.showinfuture=Show this dialog in future dynamicfield.message_text.label=Message text @@ -987,3 +1075,86 @@ dynamicfield.sender_name.label=Sender Name dynamicfield.sender_number.label=Sender Number dynamicfield.recipient_number.label=Recipient Number dynamicfield.recipient_name.label=Recipient Name +# Smpp Fconnection +smpp.label=SMPP Account +smpp.type.label=Type +smpp.name.label=Name +smpp.send.label=Use for sending +smpp.receive.label=Use for receiving +smpp.url.label=SMSC URL +smpp.port.label=SMSC Port +smpp.username.label=Username +smpp.password.label=Password +smpp.fromNumber.label=From number +smpp.description=Send and receive messages through an SMSC +smpp.global.info=You will need to get an account with your phone network of choice. +smpp.send.validator.invalid=You cannot configure a connection without SEND or RECEIVE fuctionality. +routing.title=Create rules for which phone number is used by outgoing messages. +routing.info=These rules will determine how the system selects which connection or phone number to use to send outgoing messages. Remember, the phone number seen by recipients may depend on the rules you set here. Also, changing this configuration may affect the cost of sending messages. +routing.rules.sending=When sending outgoing messages: +routing.rules.not_selected=If none of the above rules match: +routing.rules.otherwise=Otherwise: +routing.rules.device=Use {0} +routing.rule.uselastreceiver=Send through most recent number that the contact messaged +routing.rule.useany=Use any available connection's phone number +routing.rule.dontsend=Do not send the message +routing.notification.no-available-route=Outgoing message(s) not sent due to your routing preferences. +routing.rules.none-selected.warning=Warning: You have no rules or phone numbers selected. No messages will be sent. If you wish to send messages, please enable a connection. +customactivity.overview=Overview +customactivity.title={0} +customactivity.confirm=Confirm +customactivity.label=Custom Activity Builder +customactivity.description=Create your own activity from scratch by applying a custom set of actions to your specified keyword +customactivity.name.prompt=Name this activity +customactivity.moreactions.delete=Delete activity +customactivity.moreactions.rename=Rename activity +customactivity.moreactions.edit=Edit activity +customactivity.moreactions.export=Export activity +customactivity.text.none=None +customactivity.config=Configure +customactivity.config.description=Build and configure a set of actions for this activity. The actions will all be executed when a message matches the criteria you set on the previous step. +customactivity.info=Your Custom Activity has been created, and any messages containing your keyword will have the specified actions applied to it. +customactivity.info.warning=Without a keyword, all incoming messages will trigger the actions in this Custom Activity. +customactivity.info.note=Note: If you archive the Custom Activity, incoming messages will no longer be sorted for it. +customactivity.save.success={0} activity saved +customactivity.action.steps.label=Action Steps +validation.group.notnull=Please select a group +customactivity.join.description=Joining "{0}" group +customactivity.leave.description=Leaving "{0}" group +customactivity.forward.description=Forwarding with "{0}" +customactivity.webconnectionStep.description=Upload to "{0}" +customactivity.reply.description=Reply with "{0}" +customactivity.step.join.add=Add sender to group +customactivity.step.join.title=Add sender to group* +customactivity.step.leave.add=Remove sender from group +customactivity.step.leave.title=Remove sender from group* +customactivity.step.reply.add=Send Autoreply +customactivity.step.reply.title=Enter message to autoreply to sender* +customactivity.step.forward.add=Forward message +customactivity.step.forward.title=Automatically forward a message to one or more contacts +customactivity.manual.sorting=Automatic processing disabled +customactivity.step.webconnectionStep.add=Upload message to a URL +customactivity.step.webconnectionStep.title=Upload message to a URL +customactivity.validation.error.autoreplytext=Reply message is required +customactivity.validation.error.name=Name is required +customactivity.validation.error.url=URL is required +customactivity.validation.error.paramname=Parameter name is required +recipientSelector.keepTyping=Keep typing... +recipientSelector.searching=Searching... +validation.recipients.notnull=Please select at least one recipient +localhost.ip.placeholder=your-ip-address +# Missed Calls +missedCall.header=Missed Calls +missedCall.section.inbox=Received +missedCall.header.inbox=Received Missed Calls +missedCall.none=No missed calls have been received yet. +missedCall.displaytext=Missed call from {0} +missedCall.reply=Reply by SMS +missedCall.label=missed call +missedCall.label.multiple={0} missed calls +missedCall.new.info=You have {0} new missed calls. Click to view +# Multiple select +missedCall.multiple.selected={0} missed calls selected +message.multiple.selected={0} messages selected +fconnection.name.blank.error=Connection name cannot be blank +fconnection.frontlinesync.googleplaystore.download=To download, please visit the Google Play Store diff --git a/plugins/frontlinesms-core/grails-app/i18n/messages_ar.properties b/plugins/frontlinesms-core/grails-app/i18n/messages_ar.properties index 0c6fd4be0..13fc9e356 100644 --- a/plugins/frontlinesms-core/grails-app/i18n/messages_ar.properties +++ b/plugins/frontlinesms-core/grails-app/i18n/messages_ar.properties @@ -1,9 +1,6 @@ -# FrontlineSMS English translation by the FrontlineSMS team, Nairobi -language.name=Arabic - +language.name=العربية # General info app.version.label=Version - # Common action imperatives - to be used for button labels and similar action.ok=موافق action.close=إغلاق @@ -16,63 +13,53 @@ action.create=إنشاء action.edit=تعديل action.rename=إعادة التسمية action.save=حفظ -action.save.all=حفظ جميع التغييرات +action.save.all=Save Selected action.delete=حذف -action.delete.all=حذف جميع التغييرات +action.delete.all=Delete Selected action.send=إرسال action.export=تصدير - # Messages when FrontlineSMS server connection is lost server.connection.fail.title=تم فقدان الاتصال بالخادم server.connection.fail.info=الرجاء إعادة تشغيل برنامج فرونت لاين اس ام اس، اواغلق الويندو - #Connections: connection.creation.failed={0} تعذر إنشاء الاتصال -connection.route.destroyed={1} {0} الخط معطل من -connection.route.connecting=جاري الإتصال -connection.route.disconnecting=جاري فصل الإتصال +connection.route.disabled={1} {0} الخط معطل من connection.route.successNotification={0} تم انشاء الخط بنجاح -connection.route.failNotification={1}: {2} فشل انشاء الخط [edit] -connection.route.destroyNotification={0} الخط مقطوع -connection.test.sent= {0} بواسطة {1} رسالة اختبار ارسلت بنجاح الى +connection.route.failNotification=Failed to create connection on {1}: {2} [[[edit]](({0}))]. +connection.route.disableNotification={0} الخط مقطوع +connection.test.sent={0} بواسطة {1} رسالة اختبار ارسلت بنجاح الى # Connection exception messages connection.error.org.smslib.alreadyconnectedexception=الجهاز مفصول connection.error.org.smslib.gsmnetworkregistrationexception=GSM فشل في التسجيل مع شبكة connection.error.org.smslib.invalidpinexception=الرقم السري المرفق غير صحيح connection.error.org.smslib.nopinexception=الرقم السري ضروري ولكنه غير مرفق connection.error.java.io.ioexception={0} المنفذ اصدر رسالة خلل - -connection.header=اتصالات +connection.header=Settings > Connections connection.list.none=لا يوجد اتصال مهئ connection.edit=تعديل الاتصال connection.delete=حذف الاتصال connection.deleted={0} حذف الاتصال -connection.route.create=انشاء خط connection.add=إضافة إتصال جديد connection.createtest.message.label=رسالة -connection.route.destroy=تدمير الخط +connection.route.disable=تدمير الخط connection.send.test.message=إرسال رسالة اختبار connection.test.message=تهانينا من فرونت لاين اس ام اس \\o/ SMS تم بنجاح اعداد {0} لارسال رسالة \\o/ connection.validation.prompt=يرجى تعبئة جميع البيانات المطلوبة connection.select=حدد نوع الاتصال -connection.type=إختر نوع الاتصال +connection.type=إختر نوع الاتصال connection.details=أدخل التفاصيل connection.confirm=تأكيد connection.createtest.number=الرقم connection.confirm.header=تأكيد الإعدادات -connection.name.autoconfigured={الإعداد التلقائي {0} {1} على خط {2 - -status.connection.header=اتصالات +connection.name.autoconfigured={الإعداد التلقائي {0} {1} على خط {2 status.connection.none=لا توجد اتصالات معدة status.devises.header=أجهزة مكتشفة status.detect.modems=إكتشاف مودمز status.modems.none=لم يتم اكتشاف أي جهاز - connectionstatus.not_connected=لا يوجد اتصال connectionstatus.connecting=جاري الإتصال connectionstatus.connected=تم الإتصال - -default.doesnt.match.message=Property [{0}] of class [{1}] with value [{2}] does not match the required pattern [{3}] +default.doesnt.match.message=Property [{0}] of class [{1}] with value [{2}] does not match the required pattern [{3}] default.invalid.url.message=Property [{0}] of class [{1}] with value [{2}] is not a valid URL default.invalid.creditCard.message=Property [{0}] of class [{1}] with value [{2}] is not a valid credit card number default.invalid.email.message=Property [{0}] of class [{1}] with value [{2}] is not a valid e-mail address @@ -88,39 +75,33 @@ default.blank.message=Property [{0}] of class [{1}] cannot be blank default.not.equal.message=Property [{0}] of class [{1}] with value [{2}] cannot equal [{3}] default.null.message=Property [{0}] of class [{1}] cannot be null default.not.unique.message=Property [{0}] of class [{1}] with value [{2}] must be unique - default.paginate.prev=السابق default.paginate.next=التالي default.boolean.true=صحيح default.boolean.false=خاطئ default.date.format=dd MMMM, yyyy hh:mm default.number.format=0 - default.unarchived={0} غير مؤرشف default.unarchive.failed={0} فشل في عدم الأرشفة -default.trashed={0} مهمل +default.trashed={0} moved to trash default.restored={0} مستعاد default.restore.failed={لم يكن بالإمكان استعادة {0} مع {1 -default.archived={0} نجحت الأرشفة +default.archived={0} archived default.archived.multiple={0} مؤرشف default.created={0} تم الإنشاء default.created.message={0} {1} تم الإنشاء default.create.failed={0} فشل في انشاء default.updated={0} تم تحديث default.update.failed={1} فشل في تحديث {0} مع -default.updated.multiple= {0} تم تحديث +default.updated.multiple={0} تم تحديث default.updated.message={0} تم تحديث default.deleted={0} تم حذف -default.trashed={0} في سلة المهملات default.trashed.multiple={0} في سلة المهملات -default.archived={0} مؤرشف -default.unarchived={0} غير مؤرشف -default.unarchive.keyword.failed= {0} فشل في عدم أرشفة. الكلمة قيد الاستخدام +default.unarchive.keyword.failed={0} فشل في عدم أرشفة. الكلمة قيد الاستخدام default.unarchived.multiple={0} غير مؤرشف default.delete.failed={1} لم يكن بالإمكان حذف {0} مع default.notfound={1} لم يكن بالإمكان ايجاد {0} مع default.optimistic.locking.failure=تم تحديث {0} عن طريق مستخدم اخر خلال عملية التعديل - default.home.label=الصفحة الرئيسية default.list.label={0} القائمة default.add.label={0} أضف @@ -129,7 +110,6 @@ default.create.label={0} انشاء default.show.label={0} عرض default.edit.label={0} تعديل search.clear=استعادة البحث - default.button.create.label=إنشاء default.button.edit.label=تعديل default.button.update.label=تحديث @@ -137,65 +117,29 @@ default.button.delete.label=حذف default.button.search.label=بحث default.button.apply.label=تطبيق default.button.delete.confirm.message=هل انت متأكد؟ - default.deleted.message={0} تم حذف - # Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) -typeMismatch.java.net.URL= صالحا URL خاصية {0} يجب ان يكون عنوان +typeMismatch.java.net.URL=صالحا URL خاصية {0} يجب ان يكون عنوان typeMismatch.java.net.URI=صالحا URI خاصية {0} يجب ان يكون عنوان -typeMismatch.java.util.Date=يجب ان يكون تاريخا صالحا +typeMismatch.java.util.Date=يجب ان يكون تاريخا صالحا typeMismatch.java.lang.Double=يجب ان يكون رقما صالحا typeMismatch.java.lang.Integer=يجب ان يكون رقما صالحا typeMismatch.java.lang.Long=يجب ان يكون رقما صالحا typeMismatch.java.lang.Short=يجب ان يكون رقما صالحا typeMismatch.java.math.BigDecimal=يجب ان يكون رقما صالحا typeMismatch.java.math.BigInteger=يجب ان يكون رقما صالحا -typeMismatch.int = يجب ان يكون رقما صالحا - +typeMismatch.int=يجب ان يكون رقما صالحا # Application specific messages messages.trash.confirmation=هذا سوف يفرغ سلة المهملات ويحذف الرسائل. هل تريد المتابعة؟ default.created.poll=تم انشاء الاقتراع default.search.label=استعادة البحث default.search.betweendates.title=بين تاريخ default.search.moresearchoption.label=مزيد من إختيارات البحث -default.search.date.format=d/M/yyyy +default.search.date.format=d/M/yyyy default.search.moreoption.label=مزيد من الاختيارات - -# SMSLib Fconnection -smslibfconnection.label=تلفون/مودم -smslibfconnection.type.label=النوع -smslibfconnection.name.label=الاسم -smslibfconnection.port.label=المنفذ -smslibfconnection.baud.label=معدل الباود -smslibfconnection.pin.label=الرقم السري -smslibfconnection.imsi.label=SIM IMSI -smslibfconnection.serial.label=الرقم التسلسلي للجهاز -smslibfconnection.send.label=استخدم المودم لإرسال رسائل فقط -smslibfconnection.receive.label =استخدم المودم لاستقبال رسائل فقط -smslibFconnection.send.validator.error.send=يجب استخدام المودم للإرسال -smslibFconnection.receive.validator.error.receive=او استقبال الرسائل - -# Email Fconnection -emailfconnection.label=إيميل -emailfconnection.type.label= النوع -emailfconnection.name.label= الاسم -emailfconnection.receiveProtocol.label=بروتوكول -emailfconnection.serverName.label=اسم الخادم -emailfconnection.serverPort.label=اسم المنفذ -emailfconnection.username.label=اسم المستخدم -emailfconnection.password.label=كلمة السر - -# CLickatell Fconnection -clickatellfconnection.label=حساب clickatell -clickatellfconnection.type.label=النوع -clickatellfconnection.name.label= الاسم -clickatellfconnection.apiId.label=API ID -clickatellfconnection.username.label=اسم المستخدم -clickatellfconnection.password.label=كلمة السر - # Messages Tab message.create.prompt=ادخل الرسالة -message.character.count= (SMS الأحرف المتبقية {0} ({1} رسالة +message.character.count=(SMS الأحرف المتبقية {0} ({1} رسالة message.character.count.warning=ربما الانتظار أطول بعد عملية الاستبدال announcement.label=اعلان announcement.description=ابعث برسالة إعلانية ونظم الاستجابات @@ -213,7 +157,7 @@ announcement.details.label=تأكيد التفاصيل announcement.message.label=رسالة announcement.message.none=لا شئ announcement.recipients.label=المستلمين -announcement.create.message=انشأ رسالة +announcement.create.message=انشأ رسالة #TODO embed javascript values announcement.recipients.count=جهة الاتصال المختارة announcement.messages.count=سيتم إرسال الرسائل @@ -222,8 +166,6 @@ announcement.moreactions.rename=اعادة تسمية الاعلان announcement.moreactions.edit=تعديل الاعلان announcement.moreactions.export=تصدير الاعلان frontlinesms2.Announcement.name.unique.error.frontlinesms2.Announcement.name={1} {0} "{2}" يجب ان تكون فريدة من نوعها - - archive.inbox=أرشيف البريد الوارد archive.sent=أرشيف البريد المرسل archive.activity=أرشيف الانشطة @@ -232,10 +174,10 @@ archive.folder.name=الاسم archive.folder.date=التاريخ archive.folder.messages=الرسائل archive.folder.none=  لا يوجد أجندة مؤرشفة -archive.activity.name= الاسم +archive.activity.name=الاسم archive.activity.type=النوع -archive.activity.date= التاريخ -archive.activity.messages= الرسائل +archive.activity.date=التاريخ +archive.activity.messages=الرسائل archive.activity.list.none=  لا يوجد أجندة مؤرشفة autoreply.enter.keyword=ادخل الكلمة الاساسية autoreply.create.message=ادخل الرسالة @@ -248,11 +190,11 @@ autoreply.description=الرد التلقائي للرسائل الواردة autoreply.info=تم انشاء الرد التلقائي، اي رسالة تحتوي على كلمتك الاساسية سوف يتم إضافتها لنشاط الرد التلقائي والذي يمكن مشاهدته عن طريق الضغط عليه في القائمة اليمنى autoreply.info.warning=الرد التلقائي والذي لا يحتوي على كلمة أساسية سوف يتم إرساله الى جميع الرسائل الواردة autoreply.info.note=اذا قمت بأرشفة الرد التلقائي، فسوف لن يتم فرز الرسائل الواردة لذلك -autoreply.validation.prompt= يرجى تعبئة جميع الحقول المطلوبة +autoreply.validation.prompt=يرجى تعبئة جميع الحقول المطلوبة autoreply.message.title=سوف يتم اعادة إرسال الرسالة لهذا الرد التلقائي autoreply.keyword.title=فرز الرسائل تلقائيا باستخدام الكلمة الاساسية autoreply.name.prompt=اختر اسما لهذا الرد التلقائي -autoreply.message.count= (واحدة SMS لا يوجد احرف (رسالة +autoreply.message.count=(واحدة SMS لا يوجد احرف (رسالة autoreply.moreactions.delete=احذف الرد التلقائي autoreply.moreactions.rename=اعادة تسمية الرد التلقائي autoreply.moreactions.edit=تعديل الرد التلقائي @@ -263,7 +205,6 @@ frontlinesms2.Autoreply.name.unique.error.frontlinesms2.Autoreply.name={1} {0} " frontlinesms2.Autoreply.name.validator.error.frontlinesms2.Autoreply.name=اسم الرد التلقائي يجب ان يكون فريدا من نوعه frontlinesms2.Keyword.value.validator.error.frontlinesms2.Autoreply.keyword.value=الكلمة الاساسية "{2}" قيد الاستخدام frontlinesms2.Autoreply.autoreplyText.nullable.error.frontlinesms2.Autoreply.autoreplyText=يجب ان تكون الرسالة غير فارغة - contact.new=جهة اتصال جديدة contact.list.no.contact=لا يوجد جهة اتصال هنا contact.header=جهات الاتصال @@ -277,29 +218,26 @@ contact.add.to.group=اضافة مزيد من المعلومات... contact.remove.from.group=انشاء جديد... contact.customfield.addmoreinformation=الاسم contact.customfield.option.createnew=انشاء جديد... -contact.name.label=الاسم -contact.phonenumber.label=موبايل -contact.phonenumber.international.warning=هذا الرقم لا يتبع الشكل الدولي. وهذا قد يسبب مشاكل في مطابقة الرسائل للأسماء +contact.name.label=الإسم +contact.phonenumber.label=رقم الهاتف contact.notes.label=الملاحظات -contact.email.label=الايميل +contact.email.label=بريد إلكتروني contact.groups.label=المجموعات contact.notinanygroup.label=ليس تابعا لأي مجموعة contact.messages.label=الرسائل -contact.sent.messages={0} الرسائل المرسلة +contact.messages.sent={0} الرسائل المرسلة contact.received.messages={0} الرسائل الواردة contact.search.messages=ابحث عن الرسائل - group.rename=اعادة تسمية المجموعة group.edit=تعديل المجموعة group.delete=حذف المجموعة group.moreactions=اعمال إضافية... - customfield.validation.prompt=الرجاء تعبئة الاسم customfield.name.label=الاسم export.contact.info=لتصدير الاسماء من فونت لاين اس ام اس، اختر نوع التصدير والمعلومات التي ينبغي إدراجها في البيانات التي تم تصديرها export.message.info=لتصدير الاسماء من فونت لاين اس ام اس، اختر نوع التصدير والمعلومات التي ينبغي إدراجها في البيانات التي تم تصديرها. @@@@@@@@@@@ export.selectformat=اختر الشكل المخرج -export.csv= شكل CSV لاستخدامه في الجدول +export.csv=شكل CSV لاستخدامه في الجدول export.pdf=شكل PDF لاستخدامه في الطباعة folder.name.label=الاسم group.delete.prompt=هل انت متأكد انك تريد حذف {0}؟ تحذير: هذه العملية لا يمكن اعادة التراجع عنها @@ -308,7 +246,7 @@ activities.header=الانشطة activities.create=انشاء نشاط جديد folder.header=الأجندات folder.create=انشاء أجندة جديدة -folder.label=أجندة +folder.label=مجلد message.folder.header={0} أجندات fmessage.trash.actions=إجراءات للإهمال... fmessage.trash.empty=تفريق سلة المهملات @@ -318,14 +256,12 @@ fmessage.responses.total={0} مجموع الردود fmessage.label=رسالة fmessage.label.multiple={0} رسائل poll.prompt=اختر اسما لهذا الاستطلاع -poll.details.label=Confirm details +poll.details.label=تأكيد التفاصيل poll.message.label=رسالة poll.choice.validation.error.deleting.response=لا يمكن ان يحتوي الاختيار المحفوظ على قيمة فارغة poll.alias=الاسماء المستعارة poll.aliases.prompt=ادخل الاسماء المستعارة للخيارات المطابقة -poll.aliases.prompt.details=يمكنك ان تدخل عدة اسماء مستعارة لكل خيار، مفصولة بفواصل. الاسم المستعار الاول سيتم إرساله عبر رسالة تعليمات الاستطلاع -poll.alias.validation.error=الاسماء المستعارة يجب ان تكون فريدة من نوعها -poll.sort.label= الفرز التلقائي +poll.sort.label=الفرز التلقائي poll.autosort.no.description=لن يتم فرز الرسائل بشكل تلقائي poll.autosort.description=فرز الرسائل مع الكلمة الاساسية poll.sort.keyword=الكلمة الاساسية @@ -345,329 +281,275 @@ poll.messages.sent={0} تم إرسال الرسائل poll.response.enabled=تم تفعيل الرد التلقائي poll.message.edit=تعديل الرسالة لإرسالها الى المستلمين poll.message.prompt=سيتم إرسال الرسالة التالية الى مستلهمين الاستطلاع -poll.message.count=واحدة SMS باقي الأحرف 160 رسالة - +poll.message.count=واحدة SMS باقي الأحرف 160 رسالة poll.moreactions.delete=حذف الاستطلاع poll.moreactions.rename=اعادة تسمية الاستطلاع poll.moreactions.edit=تعديل الاستطلاع poll.moreactions.export=تصدير الاستطلاع - #TODO embed javascript values -poll.reply.text=Reply "نعم {0} {1} " , "{2} {3} كلا " -poll.reply.text1={0} "{1} {2}" for {3} +poll.reply.text=Reply نعم "{0}", "{1}" كلا poll.reply.text2=الرجاء الإجابة ب "نعم" أو "كلا" -poll.reply.text3= أو -poll.reply.text4={0} {1} -poll.reply.text5=الإجابة -poll.reply.text6=الرجاء الإجابة +poll.reply.text3=أو +poll.reply.text5=الإجابة +poll.reply.text6=الرجاء الإجابة poll.message.send={0} {1} -poll.recipients.validation.error=حدد جهة الإتصال لإرسال الرسائل -frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name = {1} {0} "{2}" يجب أن يكون فريد من نوعه -frontlinesms2.Poll.responses.validator.error.frontlinesms2.Poll.responses=الأجوبة المحتملة لا يمكن أن تكون متطابقة -frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value = الكلمة المفتاح "{2}" في قيد الإستعمال - -wizard.title.new=جديد -wizard.fmessage.edit.title= {0} تعديل +poll.recipients.validation.error=حدد جهة الإتصال لإرسال الرسائل +frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name={1} {0} "{2}" يجب أن يكون فريد من نوعه +frontlinesms2.Poll.responses.validator.error.frontlinesms2.Poll.responses=الأجوبة المحتملة لا يمكن أن تكون متطابقة +frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value=الكلمة المفتاح "{2}" في قيد الإستعمال +wizard.title.new=جديد +wizard.fmessage.edit.title={0} تعديل popup.title.saved={0} تم الحفظ ! -popup.activity.create=خلق نشاط جديد: حدد النوع +popup.activity.create=خلق نشاط جديد: حدد النوع popup.smartgroup.create=إنشاء مجموعة ذكية -popup.help.title=مساعدة +popup.help.title=مساعدة smallpopup.customfield.create.title=إنشاء حقل مخصص -smallpopup.group.rename.title=إعادة تسمية المجموعة -smallpopup.group.edit.title=تعديل المجموعة -smallpopup.group.delete.title=حذف المجموعة -smallpopup.fmessage.rename.title= {0} إعادة تسمية -smallpopup.fmessage.delete.title= {0} حذف -smallpopup.fmessage.export.title=تصدير -smallpopup.delete.prompt= {0} حذف ? -smallpopup.delete.many.prompt= حذف {0} جهة الإتصال ؟ -smallpopup.empty.trash.prompt=إفراغ سلة المهملات? -smallpopup.messages.export.title= (تصدير النتائج ( {0} رسالة -smallpopup.test.message.title=رسالة إختبار -smallpopup.recipients.title= المستلمين -smallpopup.folder.title=مجلد -smallpopup.group.title=مجموعة -smallpopup.contact.export.title=تصدير -smallpopup.contact.delete.title=حذف -contact.selected.many=تم تحديد {0} جهة إتصال +smallpopup.group.rename.title=إعادة تسمية المجموعة +smallpopup.group.edit.title=تعديل المجموعة +smallpopup.group.delete.title=حذف المجموعة +smallpopup.fmessage.rename.title={0} إعادة تسمية +smallpopup.fmessage.delete.title={0} حذف +smallpopup.fmessage.export.title=تصدير +smallpopup.delete.prompt={0} حذف ? +smallpopup.delete.many.prompt=حذف {0} جهة الإتصال ؟ +smallpopup.empty.trash.prompt=إفراغ سلة المهملات? +smallpopup.messages.export.title=(تصدير النتائج ( {0} رسالة +smallpopup.test.message.title=رسالة إختبار +smallpopup.recipients.title=المستلمين +smallpopup.folder.title=مجلد +smallpopup.contact.export.title=تصدير +smallpopup.contact.delete.title=حذف +contact.selected.many=تم تحديد {0} جهة إتصال group.join.reply.message=مرحبا group.leave.reply.message=وداعا -fmessage.new.info=لديك {0} رسائل جديدة. انقر للمعاينة -wizard.quickmessage.title=رسالة سريعة -wizard.messages.replyall.title=الرد على الكل -wizard.send.message.title=إرسال رسالة +fmessage.new.info=لديك {0} رسائل جديدة. انقر للمعاينة +wizard.quickmessage.title=Send Message +wizard.messages.replyall.title=الرد على الكل +wizard.send.message.title=إرسال رسالة wizard.ok=حسنا wizard.create=خلق wizard.send=إرسال -common.settings=إعدادات -common.help=مساعدة - +common.settings=إعدادات +common.help=مساعدة activity.validation.prompt=يرجى تعبئة جميع الحقول المطلوبة -autoreply.blank.keyword=الكلمة المفتاحية فارغة. سيتم إرسال إجابة إلى جميع الرسائل الواردة - - -poll.type.prompt=إختر نوع الإستطلاع الذي تريد إنشائه +autoreply.blank.keyword=الكلمة المفتاحية فارغة. سيتم إرسال إجابة إلى جميع الرسائل الواردة +poll.type.prompt=إختر نوع الإستطلاع الذي تريد إنشائه poll.question.yes.no=الإجابة على السؤال من نوع "نعم/كلا" -poll.question.multiple=الإجابة على السؤال من نوع "متعدد الاختيارات" (مثلاً : "أحمر" ، "أخضر"، "أزرق" ) -poll.question.prompt=أدخل السؤال -poll.message.none=(لا ترسل رسالة لهذا السطلاع (إجمع الأجوبة فقط +poll.question.multiple=الإجابة على السؤال من نوع "متعدد الاختيارات" (مثلاً : "أحمر" ، "أخضر"، "أزرق" ) +poll.question.prompt=أدخل السؤال +poll.message.none=(لا ترسل رسالة لهذا السطلاع (إجمع الأجوبة فقط poll.replies.header=(رد تلقائي على اجوبة الإستطلاع (إختياري -poll.replies.description=عندما يتم تحديد الرسالة الواردة على أنها رد على الإستطلاع، قم بإرسال رسالة إلى المرسل -poll.autoreply.send=أرسل رد تلقائي إلى جميع اجوبة الإستطلاع +poll.replies.description=عندما يتم تحديد الرسالة الواردة على أنها رد على الإستطلاع، قم بإرسال رسالة إلى المرسل +poll.autoreply.send=أرسل رد تلقائي إلى جميع اجوبة الإستطلاع poll.responses.prompt=(أدخل جميع الأجوبة المحتملة (بين ٢ و ٥ poll.sort.header=(رتب جميع الرسائل حسب الكلمة المفتاح (إختياري -poll.sort.description=إذا قام الناس بإستعمال الكلمة المفتاح عند إرسال اجوبة على الإستطلاع، يمكن ل "فرنتلين سمس " أن يقوم بترتيب تلقائي للرسائل في النظام -poll.no.automatic.sort=لا ترتب الرسائل تلقائياً -poll.sort.automatically=رتب الرسائل تلقائياً إذا كانت تحتوي على الكلمة المفتاح التالية -poll.validation.prompt=الرجاء ملء جميع الحقول المطلوبة -poll.name.validator.error.name=إسم الإستطلاع يجب أن يكون فريد -pollResponse.value.blank.value=جواب الإستطلاع لا يمكن أن تكون فارغة -poll.alias.validation.error.invalid.alias=كنية غير مطابقة ، حاول إدخال إسم، كلمة -poll.question=أدخل سؤال -poll.response=لائحة الأجوبة -poll.sort=ترتيب تلقائي -poll.reply=رد تلقائي -poll.edit.message=تعديل رسالة -poll.recipients=إختيار المرسل اليهم -poll.confirm=تأكيد -poll.save=تم حفظ إستطلاع الرأي -poll.messages.queue=إذا اخترت إرسال رسالة مع إستطلاع الرأي، لقد تمت زيادة الرسائل إلى لائحة الإنتظار +poll.sort.description=إذا قام الناس بإستعمال الكلمة المفتاح عند إرسال اجوبة على الإستطلاع، يمكن ل "فرنتلين سمس " أن يقوم بترتيب تلقائي للرسائل في النظام +poll.no.automatic.sort=لا ترتب الرسائل تلقائياً +poll.sort.automatically=رتب الرسائل تلقائياً إذا كانت تحتوي على الكلمة المفتاح التالية +poll.validation.prompt=الرجاء ملء جميع الحقول المطلوبة +poll.name.validator.error.name=إسم الإستطلاع يجب أن يكون فريد +pollResponse.value.blank.value=جواب الإستطلاع لا يمكن أن تكون فارغة +poll.question=أدخل سؤال +poll.response=لائحة الأجوبة +poll.sort=ترتيب تلقائي +poll.reply=رد تلقائي +poll.edit.message=تعديل رسالة +poll.recipients=إختيار المرسل اليهم +poll.confirm=تأكيد +poll.save=تم حفظ إستطلاع الرأي +poll.messages.queue=إذا اخترت إرسال رسالة مع إستطلاع الرأي، لقد تمت زيادة الرسائل إلى لائحة الإنتظار poll.messages.queue.status=قد يستغرق إرسال الرسائل بعض الوقت - وذلك يعتمد على عدد الرسائل وسرعة شبكة الإتصال -poll.pending.messages=لرؤية حالة رسالتك ، إفتح مجلد الرسائل "لائحة الإنتظار" -poll.send.messages.none=لن يتم إرسال أي رسالة -quickmessage.details.label=تأكيد التفاصيل -quickmessage.message.label=رسالة +poll.pending.messages=لرؤية حالة رسالتك ، إفتح مجلد الرسائل "لائحة الإنتظار" +poll.send.messages.none=لن يتم إرسال أي رسالة +quickmessage.details.label=تأكيد التفاصيل +quickmessage.message.label=رسالة quickmessage.message.none=لا شيء -quickmessage.recipient.label=المرسل إليه -quickmessage.recipients.label=المرسل إليهم -quickmessage.message.count=(عدد الأحرف المتبقية ( رسالة قصيرة ١ -quickmessage.enter.message=أدخل رسالة -quickmessage.select.recipients=إختر المرسل اليهم -quickmessage.confirm=تأكيد +quickmessage.recipient.label=المرسل إليه +quickmessage.recipients.label=المرسل إليهم +quickmessage.message.count=(عدد الأحرف المتبقية ( رسالة قصيرة ١ +quickmessage.enter.message=أدخل رسالة +quickmessage.select.recipients=إختر المرسل اليهم +quickmessage.confirm=تأكيد #TODO embed javascript values -quickmessage.recipients.count=لقد حددت جهة الإتصال -quickmessage.messages.count=سوف يتم إرسال الرسائل -quickmessage.count.label=عدد الرسائل : -quickmessage.messages.label=أدخل الرسالة -quickmessage.phonenumber.label=أضف رقم الهاتف +quickmessage.recipients.count=لقد حددت جهة الإتصال +quickmessage.messages.count=سوف يتم إرسال الرسائل +quickmessage.count.label=عدد الرسائل : +quickmessage.messages.label=أدخل الرسالة +quickmessage.phonenumber.label=أضف رقم الهاتف quickmessage.phonenumber.add=إضافة -quickmessage.selected.recipients=قد حددت جهة الإتصال -quickmessage.validation.prompt=الرجاء ملء جميع الحقول المطلوبة - -fmessage.number.error=سيتم حذف الأحرف من هذا الحقل عند حفظ المعلومات -search.filter.label=حدد البحث ل : -search.filter.group=حدد المجموعة -search.filter.activities=حدد النشاط/المجلد -search.filter.messages.all=تم إرسال/تلقي الكل -search.filter.inbox=تم فقط تلقي الرسائل -search.filter.sent=تم فقط إرسال الرسائل -search.filter.archive=تضمين الأرشيف +quickmessage.selected.recipients=قد حددت جهة الإتصال +quickmessage.validation.prompt=الرجاء ملء جميع الحقول المطلوبة +fmessage.number.error=Non-numeric characters in this field will be removed when saved +search.filter.label=حدد البحث ل : +search.filter.group=حدد المجموعة +search.filter.activities=حدد النشاط/المجلد +search.filter.messages.all=تم إرسال/تلقي الكل +search.filter.inbox=تم فقط تلقي الرسائل +search.filter.sent=تم فقط إرسال الرسائل +search.filter.archive=تضمين الأرشيف search.betweendates.label=بين التواريخ -search.header=بحث -search.quickmessage=رسالة سريعة -search.export=تصدير النتائج -search.keyword.label=كلمة مفتاح أو جملة -search.contact.name.label=إسم جهة الإتصال -search.contact.name=إسم جهة الإتصال -search.result.header=النتائج -search.moreoptions.label=إحتمالات أخرى -settings.general=إجمالي +search.header=بحث +search.quickmessage=Send message +search.export=تصدير النتائج +search.keyword.label=كلمة مفتاح أو جملة +search.contact.name.label=إسم جهة الإتصال +search.contact.name=إسم جهة الإتصال +search.result.header=النتائج +search.moreoptions.label=إحتمالات أخرى +settings.general=إجمالي settings.connections=الهواتف والاتصالات -settings.logs=نظام -settings.general.header=إعدادات > عام -settings.logs.header=سجل النظام -logs.none=ليس لديك أي سجل -logs.content=رسالة -logs.date=وقت +settings.logs=نظام +settings.general.header=إعدادات > عام +settings.logs.header=Settings > System Logs +logs.none=ليس لديك أي سجل +logs.content=رسالة +logs.date=وقت logs.filter.label=أعرض السجل ل- -logs.filter.anytime=جميع الأوقات -logs.filter.1day= ٢٤ ساعة الأخيرة -logs.filter.3days=الأيام الثلات الأخيرة -logs.filter.7days=الأيام السبعة الأخيرة -logs.filter.14days=الأيام الأربعة عشر الأخيرة -logs.filter.28days=الأيام الثماني والعشرون الأخيرة -logs.download.label=تنزيل سجلات النظام -logs.download.buttontext=تنزيل ألسجلات -logs.download.title=تنزيل ألسجلات لإرسالها -logs.download.continue=متابعة - -smartgroup.validation.prompt=الرجاء تعبئة جميع الحقول المطلوبة . يمكنك أن تحديد قاعدة واحدة لكل حقل -smartgroup.info=لخلق مجموعة ذكية ، إختر المعيار الذي تريد أن تطابقه ضمن أعضاء هذه المجموعة -smartgroup.contains.label=يتضمن +logs.filter.anytime=جميع الأوقات +logs.download.label=تنزيل سجلات النظام +logs.download.buttontext=تنزيل ألسجلات +logs.download.title=تنزيل ألسجلات لإرسالها +logs.download.continue=متابعة +smartgroup.validation.prompt=الرجاء تعبئة جميع الحقول المطلوبة . يمكنك أن تحديد قاعدة واحدة لكل حقل +smartgroup.info=لخلق مجموعة ذكية ، إختر المعيار الذي تريد أن تطابقه ضمن أعضاء هذه المجموعة +smartgroup.contains.label=يتضمن smartgroup.startswith.label=يبدأ ب -smartgroup.add.anotherrule=أضف قاعدة أخرى -smartgroup.name.label=إسم - -modem.port=منفذ -modem.description=الوصف -modem.locked=مقفل ؟ -traffic.header=مرور المعلومات -traffic.update.chart=تحديث الرسم البياني +smartgroup.add.anotherrule=أضف قاعدة أخرى +smartgroup.name.label=إسم +modem.port=منفذ +modem.description=الوصف +modem.locked=مقفل ؟ +traffic.header=مرور المعلومات +traffic.update.chart=تحديث الرسم البياني traffic.filter.2weeks=أظهر آخر أسبوعين traffic.filter.between.dates=بين التواريخ -traffic.filter.reset=أعد تركيب قواعد تصفية المعلومات -traffic.allgroups=أظهر جميع المجموعات -traffic.all.folders.activities=أظهر جميع النشاطات/المجلدات -traffic.sent=تم الإرسال -traffic.received=تم الإستقبال -traffic.total=المجموع - -tab.message=الرسائل -tab.archive=الأرشيف -tab.contact=جهة الإتصال +traffic.filter.reset=أعد تركيب قواعد تصفية المعلومات +traffic.allgroups=أظهر جميع المجموعات +traffic.all.folders.activities=أظهر جميع النشاطات/المجلدات +traffic.sent=تم الإرسال +traffic.received=تم الإستقبال +traffic.total=المجموع +tab.message=الرسائل +tab.archive=الأرشيف +tab.contact=جهة الإتصال tab.status=حالة -tab.search=بحث - -help.info=هذا الإصدار لا يزال في مرحلة التجربة، لذلك لا توجد وثائق مساعدة رسمية، الرجاء إستعمال منتدى المستعملين في الوقت الحالي - -# IntelliSms Fconnection -intellismsfconnection.label=حساب intellisms -intellismsfconnection.type.label=نوع -intellismsfconnection.name.label=اسم -intellismsfconnection.username.label=اسم المستخدم -intellismsfconnection.password.label=كلمة السر - -intellismsfconnection.send.label=إستخدم للإرسال -intellismsfconnection.receive.label=إستخدم للإستقبال -intellismsfconnection.receiveProtocol.label=ميفاق -intellismsfconnection.serverName.label=إسم الخادم -intellismsfconnection.serverPort.label=منفذ الخادم -intellismsfconnection.emailUserName.label=اسم المستخدم -intellismsfconnection.emailPassword.label=كلمة السر - +tab.search=بحث +help.info=هذا الإصدار لا يزال في مرحلة التجربة، لذلك لا توجد وثائق مساعدة رسمية، الرجاء إستعمال منتدى المستعملين في الوقت الحالي #Controllers -contact.label=جهة الإتصال -contact.edited.by.another.user=قام مستخدم أخر بتحديث جهة الإتصال في حين قيامك بتغير المعلومات -contact.exists.prompt=يوجد جهة إتصال لديها لهذا الرقم -contact.exists.warn=هناك جهة إتصال لديها هذا الرقم -contact.view.duplicate=عرض البيانات المكررة +contact.label=جهة الإتصال +contact.edited.by.another.user=قام مستخدم أخر بتحديث جهة الإتصال في حين قيامك بتغير المعلومات +contact.exists.prompt=يوجد جهة إتصال لديها لهذا الرقم +contact.exists.warn=هناك جهة إتصال لديها هذا الرقم +contact.view.duplicate=عرض البيانات المكررة contact.addtogroup.error=لا يمكنك أن تضيف وتلغي من المجموعة نفسها ! -contact.mobile.label=الجوال -contact.email.label=بريد إلكتروني +contact.mobile.label=الجوال fconnection.label=Fconnection fconnection.name=Fconnection -fconnection.unknown.type=إتصال غير معروف النوع : +fconnection.unknown.type=إتصال غير معروف النوع : fconnection.test.message.sent=تم إرسال رسالة إختبار ! -announcement.saved=تم حفظ الإعلان واضيفت الرسالة / الرسائل إلى قائمة الإنتظار -announcement.not.saved=لم نتمكن من حفظ الإعلان +announcement.saved=تم حفظ الإعلان واضيفت الرسالة / الرسائل إلى قائمة الإنتظار +announcement.not.saved=لم نتمكن من حفظ الإعلان announcement.id.exist.not={لم نتمكن من إيجاد الإعلان رقم {0 -autoreply.saved=تم حفظ الرد التلقائي ! autoreply.not.saved=لم يتم حفظ الرد التلقائي ! report.creation.error=خطأ أثناء إنشاء تقرير export.message.title=تصدير رسائل frontlinesms -export.database.id=رقم قاعدة البيانات +export.database.id=رقم قاعدة البيانات export.message.date.created=تاريخ الإنشاء export.message.text=نص export.message.destination.name=اسم الوجهة -export.message.destination.mobile=رقم جوال الوجهة -export.message.source.name=إسم المصدر -export.message.source.mobile=رقم جوال المصدر -export.contact.title=frontlinesms تصدير جهة الإتصال -export.contact.name=الإسم -export.contact.mobile=الجوال -export.contact.email=البريد الإلكتروني -export.contact.notes=مذكرات +export.message.destination.mobile=رقم جوال الوجهة +export.message.source.name=إسم المصدر +export.message.source.mobile=رقم جوال المصدر +export.contact.title=frontlinesms تصدير جهة الإتصال +export.contact.name=الإسم +export.contact.mobile=الجوال +export.contact.email=البريد الإلكتروني +export.contact.notes=مذكرات export.contact.groups=مجموعات export.messages.name1={0} {1} ({2} رسائل ) export.messages.name2={0} ({1} رسائل ) -export.contacts.name1=({0} مجموعة ({1} جهة إتصال -export.contacts.name2=({0} مجموعات ({1} جهات إتصال -export.contacts.name3=(جميع جهات الإتصال ( {0} جهة إتصال -folder.label=مجلد -folder.archived.successfully=!تمت إضافة المجلد إلى الأرشيف -folder.unarchived.successfully=!تم إستخراج المجلد من الأرشيف بنجاح -folder.trashed=!تم نقل الملف إلى سلة المهملات -folder.restored=!تم إسترجاع المجلد من سلة المهملات +export.contacts.name1=({0} مجموعة ({1} جهة إتصال +export.contacts.name2=({0} مجموعات ({1} جهات إتصال +export.contacts.name3=(جميع جهات الإتصال ( {0} جهة إتصال +folder.archived.successfully=!تمت إضافة المجلد إلى الأرشيف +folder.unarchived.successfully=!تم إستخراج المجلد من الأرشيف بنجاح +folder.trashed=!تم نقل الملف إلى سلة المهملات +folder.restored=!تم إسترجاع المجلد من سلة المهملات folder.exist.not={لا يمكن إيجاد ملف معرف ب {0 -folder.renamed=تمت إعادة تسمية المجلد - -group.label=مجموعة -group.name.label=إسم -group.update.success=تم تعديل المجموعة بنجاح -group.save.fail=لم تحفظ المجموعة -group.delete.fail=غير قادر على إلغاء المجموعة - -import.label=إستيراد -import.backup.label=إستيراد البيانات من نسخة احتياطية -import.prompt.type=إختر نوع البيانات لعملية الإستيراد -import.contacts=تفاصيل جهة الإتصال -import.messages=تفاصيل الرسالة -import.version1.info=لإستيراد البيانات من الإصدار الأول، الرجاء تصديرها بالإنجليزية -import.prompt=إختر ملف بيانات لاستيراده +folder.renamed=تمت إعادة تسمية المجلد +group.label=مجموعة +group.name.label=إسم +group.update.success=تم تعديل المجموعة بنجاح +group.save.fail=لم تحفظ المجموعة +group.delete.fail=غير قادر على إلغاء المجموعة +import.label=Import contacts and messages +import.backup.label=You can use the form below to import your contacts. You can also import messages that you have from a previous Frontline project. +import.prompt.type=Select the type of data you wish to import before uploading +import.messages=Messages in Frontline's CSV format +import.prompt=Select the file containing your data to initiate the import import.upload.failed=فشل تحميل الملف لسبب مجهول import.contact.save.error=تمت مواجهة مشكلة عند محاولة حفظ جهة الإتصال import.contact.complete={تم إستيراد {0} جهة إتصال؛ فشل الإستيراد ل-{1 -import.contact.failed.download=فشل تنزيل جهات الإتصال (csv) import.message.save.error=تمت مواجهة مشكلة عند حفظ الرسالة import.message.complete={تم إستيراد {0} رسالة؛ فشل {1 - -many.selected = {تم إختيار {0} و-{1 - +many.selected={تم إختيار {0} و-{1 flash.message.activity.found.not=لا يمكن إيجاد النشاط flash.message.folder.found.not=لا يمكن إيجاد المجلد flash.message=رسالة -flash.message.fmessage={0} رسالة/رسائل +flash.message.fmessage={0} رسالة/رسائل flash.message.fmessages.many={0} رسالة قصيرة flash.message.fmessages.many.one={1} رسالة قصيرة -fmessage.exist.not={لم يتم إيجاد رسالة معرفة ب-{0 -flash.message.poll.queued=تم حفظ إستطلاع الرأي وإضافة الرسائل إلى لائحة الإنتظار -flash.message.poll.saved=تم حفظ إستطلاع الرأي +fmessage.exist.not={لم يتم إيجاد رسالة معرفة ب-{0 +flash.message.poll.queued=تم حفظ إستطلاع الرأي وإضافة الرسائل إلى لائحة الإنتظار flash.message.poll.not.saved=!لم يحفظ إستطلاع الرأي system.notification.ok=حسنا system.notification.fail=فشل -flash.smartgroup.delete.unable=لا يمكن إلغاء المجموعة الذكية +flash.smartgroup.delete.unable=لا يمكن إلغاء المجموعة الذكية flash.smartgroup.saved={حفظ المجموعة الذكية {0 flash.smartgroup.save.failed={فشل في عمليات حفظ المجموعة الذكية . الأخطاء {0 -smartgroup.id.exist.not={لم يتم إيجاد المجموعة الذكية ذات التعريف {0 +smartgroup.id.exist.not={لم يتم إيجاد المجموعة الذكية ذات التعريف {0 smartgroup.save.failed={فشل حفظ المجموعة الذكية {0} ذات المعامل {1} {2} - الأخطاء {3 -contact.name.label=الإسم -contact.phonenumber.label=رقم الهاتف - searchdescriptor.searching=جاري البحث -searchdescriptor.all.messages= جميع الرسائل +searchdescriptor.all.messages=جميع الرسائل searchdescriptor.archived.messages=، بما فيه رسائل الأرشيف searchdescriptor.exclude.archived.messages=،من دون رسائل الأرشيف searchdescriptor.only=،{فقط {0 searchdescriptor.between=، {بين {0} و-{1 searchdescriptor.from={إبتداءً من {0 searchdescriptor.until=,،{ حتى {0 -poll.title={0} إستطلاع رأي -announcement.title={0} إعلان -autoreply.title={0} رد تلقائي -folder.title={0} مجلد -frontlinesms.welcome=أهلاً بك إلى frontlinesms \\o/ +poll.title={0} +announcement.title={0} +autoreply.title={0} +folder.title={0} +frontlinesms.welcome=أهلاً بك إلى frontlinesms \\o/ failed.pending.fmessages=لم يتم إرسال {0} رسالة في لائحة الإنتظار . إذهب إلى اللائحة للمراجعة - language.label=اللغة -language.prompt=تغيير لغة واجهة المستخدم +language.prompt=تغيير لغة واجهة المستخدم frontlinesms.user.support=تغيير لغة واجهة المستخدم في frontlinesms download.logs.info1=تحذير: إن فريق غير قادر على الرد المباشر على جميع ألسجلات. إذا كان لديك طلب مساعدة كمستخدم، الرجاء مراجعة ملفات المساعدة لإيجاد جواب. في حال عدم تمكنك من ذلك - الرجاء مراجعة منتدى المستخدمين على الإنترنت. - -download.logs.info2=من الممكن أن مستعملون قد قاموا بتقديم تقرير عن المشكلة نفسها وقاموا بإيجاد حل . للمتابعة وإرسال السجل إضغط على زر "تابع" - +download.logs.info2=من الممكن أن مستعملون قد قاموا بتقديم تقرير عن المشكلة نفسها وقاموا بإيجاد حل . للمتابعة وإرسال السجل إضغط على زر "تابع" dynamicfield.contact_name.label=اسم جهة الاتصال dynamicfield.contact_number.label=رقم الاتصال -dynamicfield.keyword.label=الكلمة المفتاح +dynamicfield.keyword.label=الكلمة المفتاح dynamicfield.message_content.label=محتوى الرسالة - -# Fmessage domain +# TextMessage domain fmessage.queued={تمت إضافة الرسالة إلى لائحة الإنتظار ليتم ارسلها إلى {0 -fmessage.queued.multiple=أضيفت الرسالة إلى لائحة الإنتظار لإرسالها إلى {0} متلقي +fmessage.queued.multiple=أضيفت الرسالة إلى لائحة الإنتظار لإرسالها إلى {0} متلقي fmessage.retry.success={تمت إعادة إضافة الرسالة إلى لائحة الإنتظار لإرسالها إلى {0 -fmessage.retry.success.multiple=تمت إضافة {0} رسائل إلى لائحة الإنتظار لإرسالها مجدداً +fmessage.retry.success.multiple=تمت إضافة {0} رسائل إلى لائحة الإنتظار لإرسالها مجدداً fmessage.displayName.label=إسم fmessage.text.label=رسالة fmessage.date.label=تاريخ fmessage.to={إلى {0 -fmessage.to.multiple=إلى {0} : متلقي -fmessage.quickmessage=رسالة سريعة +fmessage.to.multiple=إلى {0} : متلقي +fmessage.quickmessage=Send message fmessage.archive=أرشيف fmessage.activity.archive={أرشيف {0 -fmessage.unarchive={إلغاء الأرشفة {0 +fmessage.unarchive=إلغاء الأرشفة fmessage.export=تصدير fmessage.rename={إعادة تسمية {0 fmessage.edit={تعديل {0 -fmessage.delete={إلغاء {0 +fmessage.delete=Delete fmessage.moreactions=المزيد من الإجراءات... fmessage.footer.show=إظهار fmessage.footer.show.failed=فشل @@ -686,51 +568,44 @@ fmessage.resend=أرسل مجدداً fmessage.retry=إعادة المحاولة fmessage.reply=رد fmessage.forward=إلى الأمام -fmessage.unarchive=إلغاء الأرشفة -fmessage.delete=إلغاء fmessage.messages.none=لا يوجد رسالة هنا ! fmessage.selected.none=لم يتم إختيار رسالة -fmessage.move.to.header=...إنقل الرسالة إلى +fmessage.move.to.header=...إنقل الرسالة إلى fmessage.move.to.inbox=الرسائل الواردة -fmessage.archive.many=ارشفة الجميع +fmessage.archive.many=Archive selected fmessage.count=رسالة واحدة fmessage.count.many={0} رسائل -fmessage.many= رسائل -fmessage.delete.many=إلغاء الجميع -fmessage.reply.many=الرد على الجميع +fmessage.many=رسائل +fmessage.delete.many=Delete selected +fmessage.reply.many=Reply selected fmessage.restore=استعادة fmessage.restore.many=استعادة -fmessage.retry.many=فشلت إعادة المحاولة +fmessage.retry.many=Retry selected fmessage.selected.many=تم إختيار {0} رسالة -fmessage.unarchive.many=إلغاء ارشفت الجميع - +fmessage.unarchive.many=Unarchive selected # TODO move to poll.* fmessage.showpolldetails=عرض الرسم البياني fmessage.hidepolldetails=إخفاء الرسم البياني - # TODO move to search.* fmessage.search.none=لا توجد رسائل fmessage.search.description=ابدأ البحث الجديد على اليسار - activity.name=إسم activity.delete.prompt=إنقل {0} إلى سلة المهملات . ذلك سيؤدي إلى نقل جميع الرسائل المرطبة إلى سلة المهملات أيضاً activity.label=نشاط activity.categorize=صنف الأجوبة - magicwand.title=أضف عبارة إستبدال -folder.create.success=تم إنشاء المجلد بنجاح +folder.create.success=تم إنشاء المجلد بنجاح folder.create.failed=فشل خلق المجلد folder.name.validator.error=إسم المجلد قيد الإستعمال folder.name.blank.error=لا يمكن أن يكون إسم المجلد فارغاً -poll.name.blank.error=لا يمكن أن يكون إسم الإستطلاع فارغاً -poll.name.validator.error=إسم المجلد قيد الإستخدام +poll.name.blank.error=لا يمكن أن يكون إسم الإستطلاع فارغاً +poll.name.validator.error=إسم المجلد قيد الإستخدام autoreply.name.blank.error=إسم الرد التلقائي لا يمكن أن يكون فارغاً autoreply.name.validator.error=إسم الرد التلقائي لا يمكن أن يكون فارغاً announcement.name.blank.error=إسم الإعلان لا يمكن أن يكون فارغاً -announcement.name.validator.error=إسم الإعلان في قيد الإستعمال +announcement.name.validator.error=إسم الإعلان في قيد الإستعمال group.name.blank.error=إسم المجموعة لا يمكن أن يكون فارغاً -group.name.validator.error=إسم المجموعة قيد الإستعمال - +group.name.validator.error=إسم المجموعة قيد الإستعمال #Jquery Validation messages jquery.validation.required=هذا الحقل مطلوب. jquery.validation.remote=يرجى تصحيح هذا المجال. @@ -749,4 +624,3 @@ jquery.validation.rangelength=الرجاء إدخال قيمة بين {0} و {1} jquery.validation.range={الرجاء إدخال قيمة بين {1} و {0 jquery.validation.max={الرجاء إدخال قيمة أقل من أو يساوي {0 jquery.validation.min={الرجاء إدخال قيمة أكبر من أو يساوي {0 - diff --git a/plugins/frontlinesms-core/grails-app/i18n/messages_de.properties b/plugins/frontlinesms-core/grails-app/i18n/messages_de.properties index 95c913dfc..19549ceca 100644 --- a/plugins/frontlinesms-core/grails-app/i18n/messages_de.properties +++ b/plugins/frontlinesms-core/grails-app/i18n/messages_de.properties @@ -1,9 +1,7 @@ # FrontlineSMS English translation by the FrontlineSMS team, Nairobi language.name=Deutsch - # General info app.version.label=Version - # Common action imperatives - to be used for button labels and similar action.ok=OK action.close=Schließen @@ -16,24 +14,20 @@ action.create=Neu action.edit=Bearbeiten action.rename=Umbenennen action.save=Sichern -action.save.all=Alle sichern +action.save.all=Save Selected action.delete=Löschen -action.delete.all=Alle löschen +action.delete.all=Delete Selected action.send=Senden action.export=Exportieren - # Messages when FrontlineSMS server connection is lost server.connection.fail.title=Serververbindung wurde unterbrochen. server.connection.fail.info=Bitte starten Sie FrontlineSMS erneut, oder schließen Sie dieses Fenster. - #Connections: connection.creation.failed=Die Verbindung konnte nicht aufgebaut werden {0} -connection.route.destroyed=Die Verbindung von {0} nach {1} wurde abgebaut -connection.route.connecting=Verbindungsaufbau... -connection.route.disconnecting=Verbindungsabbau... +connection.route.disabled=Die Verbindung von {0} nach {1} wurde abgebaut connection.route.successNotification=Verbindung erstellt auf {0} -connection.route.failNotification=Die folgende Verbindung konnte nicht aufgebaut werden {1}: {2} [bearbeiten] -connection.route.destroyNotification=Die folgende Verbindung wurde getrennt {0} +connection.route.failNotification=Failed to create connection on {1}: {2} [[[edit]](({0}))]. +connection.route.disableNotification=Die folgende Verbindung wurde getrennt {0} connection.test.sent=Die Test-SMS an {0} wurde erfolgreich mit {1} gesendet # Connection exception messages connection.error.org.smslib.alreadyconnectedexception=Dieses Gerät ist schon verbunden @@ -41,16 +35,14 @@ connection.error.org.smslib.gsmnetworkregistrationexception=Registrierung mit de connection.error.org.smslib.invalidpinexception=Falsche PIN Nummer connection.error.org.smslib.nopinexception=PIN Nummer ist erforderlich connection.error.java.io.ioexception=Port-Fehlermeldung: {0} - -connection.header=Verbindungen +connection.header=Settings > Connections connection.list.none=Es sind keine Verbindungen konfiguriert. connection.edit=Verbindung bearbeiten connection.delete=Verbindung löschen connection.deleted=Verbindung {0} wurde gelöscht. -connection.route.create=Neue Verbindung connection.add=Neue Verbindung hinzufügen connection.createtest.message.label=Nachricht -connection.route.destroy=Verbindung löschen +connection.route.disable=Verbindung löschen connection.send.test.message=Test-SMS senden connection.test.message=Herzlichen Glückwunsch von FrontlineSMS \\o/ {0} ist erfolgreich zum Senden von SMS konfiguriert \\o/ connection.validation.prompt=Bitte füllen Sie alle erforderlichen Felder aus @@ -61,17 +53,13 @@ connection.confirm=Bestätigen connection.createtest.number=Nummer connection.confirm.header=Einstellungen bestätigen connection.name.autoconfigured=Automatisch konfiguriert {0} {1} on port {2}" - -status.connection.header=Verbindungen status.connection.none=Es sind keine Verbindungen konfiguriert. status.devises.header=Gefundene Geräte status.detect.modems=Modems suchen status.modems.none=Bisher wurden noch keine Geräte gefunden. - connectionstatus.not_connected=Nicht verbunden connectionstatus.connecting=Verbindungsaufbau connectionstatus.connected=Verbunden - default.doesnt.match.message=Eigenschaft [{0}] der Klasse [{1}] mit dem Wert [{2}] passt nicht zum erforderlichen Muster [{3}] default.invalid.url.message=Eigenschaft [{0}] der Klasse [{1}] mit dem Wert [{2}] ist keine gültige URL default.invalid.creditCard.message=Eigenschaft [{0}] der Klasse [{1}] mit dem Wert [{2}] ist keine valide Kreditkartennummer @@ -88,39 +76,33 @@ default.blank.message=Eigenschaft [{0}] der Klasse [{1}] darf nicht leer sein default.not.equal.message=Eigenschaft [{0}] der Klasse [{1}] mit dem Wert [{2}] muss ungleich [{3}] sein default.null.message=Eigenschaft [{0}] der Klasse [{1}] darf nicht Null sein default.not.unique.message=Eigenschaft [{0}] der Klasse [{1}] mit dem Wert [{2}] muss eindeutig sein - default.paginate.prev=Zurück default.paginate.next=Weiter default.boolean.true=True default.boolean.false=False default.date.format=dd MMMM yyyy hh:mm default.number.format=0 - default.unarchived={0} wurde wieder aktiviert default.unarchive.failed=Aktivierung von {0} ist fehlgeschlagen -default.trashed={0} wurde in den Papierkorp gelegt +default.trashed={0} moved to trash default.restored={0} wurde wiederhergestellt default.restore.failed={0} konnte nicht wiederhergestellt werden. Fehler {1} -default.archived={0} wurde erfolgreich archiviert +default.archived={0} archived default.archived.multiple={0} wurden archiviert default.created={0} wurde erstellt default.created.message={0} {1} wurde neu erstellt default.create.failed={0} konnte nicht neu erstellt werden default.updated={0} wurde aktualisiert default.update.failed={0} konnte nicht aktualisiert werden. Fehler {1} -default.updated.multiple= {0} wurden aktualisiert +default.updated.multiple={0} wurden aktualisiert default.updated.message={0} aktualisiert default.deleted={0} wurde gelöscht -default.trashed={0} wurde in den Papierkorb gelegt. default.trashed.multiple={0} wurden in den Papierkorb gelegt. -default.archived={0} wurde archiviert -default.unarchived={0} wurde wieder aktiviert default.unarchive.keyword.failed={0} konnte nicht wieder aktiviert werden. Das Stichwort gibt es schon default.unarchived.multiple={0} wurden wieder aktiviert. default.delete.failed={0} konnte nicht gelöscht werden. Fehler {1} default.notfound={0} konnte nicht gefunden werden. Fehler {1} default.optimistic.locking.failure={0} wurde gleichzeitig von einem anderen Benutzer bearbeitet - default.home.label=Home default.list.label={0}Liste default.add.label={0} hinzufügen @@ -129,7 +111,6 @@ default.create.label={0} neu erstellen default.show.label={0} anzeigen default.edit.label={0} bearbeiten search.clear=Suche löschen - default.button.create.label=Erstellen default.button.edit.label=Bearbeiten default.button.update.label=Aktualisieren @@ -137,9 +118,7 @@ default.button.delete.label=Löschen default.button.search.label=Suchen default.button.apply.label=Anwenden default.button.delete.confirm.message=Sind Sie sicher? - default.deleted.message={0} gelöscht - # Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) typeMismatch.java.net.URL=Eigenschaft {0} muss eine valide URL sein typeMismatch.java.net.URI=Eigenschaft {0} muss eine valide URI sein @@ -151,48 +130,14 @@ typeMismatch.java.lang.Short=Eigenschaft {0} muss eine valide Zahl sein typeMismatch.java.math.BigDecimal=Eigenschaft {0} muss eine valide Zahl sein typeMismatch.java.math.BigInteger=Eigenschaft {0} muss eine valide Zahl sein typeMismatch.int={0} muss eine valide Zahl sein - # Application specific messages messages.trash.confirmation=Hiermit werden alle SMS im Papierkorb endgültig gelöscht. Fortfahren? default.created.poll=Die Umfrage wurde erstellt default.search.label=Suche löschen default.search.betweendates.title=Im Zeitraum: default.search.moresearchoption.label=Weitere Suchoptionen -default.search.date.format=d.M.yyyy +default.search.date.format=d.M.yyyy default.search.moreoption.label=Weitere Optionen - -# SMSLib Fconnection -smslibfconnection.label=Telefon/Modem -smslibfconnection.type.label=Typ -smslibfconnection.name.label=Name -smslibfconnection.port.label=Port -smslibfconnection.baud.label=Baud Rate -smslibfconnection.pin.label=PIN -smslibfconnection.imsi.label=SIM IMSI -smslibfconnection.serial.label=Seriennummer des Gerätes -smslibfconnection.send.label=Nutze das Modem zum Senden von Messages ONLY. -smslibfconnection.receive.label=Nutze das Modem zum Empfang von Messages ONLY -smslibFconnection.send.validator.error.send=Das Modem sollte zum Senden verwendet werden -smslibFconnection.receive.validator.error.receive=oder zum Empfangen. - -# Email Fconnection -emailfconnection.label=E-Mail -emailfconnection.type.label=Typ -emailfconnection.name.label=Name -emailfconnection.receiveProtocol.label=Protokoll -emailfconnection.serverName.label=Server Name -emailfconnection.serverPort.label=Server Port -emailfconnection.username.label=Benutzername -emailfconnection.password.label=Passwort - -# CLickatell Fconnection -clickatellfconnection.label=Clickatell Account -clickatellfconnection.type.label=Typ -clickatellfconnection.name.label=Name -clickatellfconnection.apiId.label=API ID -clickatellfconnection.username.label=Benutzername -clickatellfconnection.password.label=Passwort - # Messages Tab message.create.prompt=SMS verfassen message.character.count={0} verbleibende Zeichen ({1} SMS). @@ -222,17 +167,15 @@ announcement.moreactions.rename=Mitteilung umbenennen announcement.moreactions.edit=Mitteilung bearbeiten announcement.moreactions.export=Mitteilung exportieren frontlinesms2.Announcement.name.unique.error.frontlinesms2.Announcement.name={1} {0} "{2}" muss eindeutig sein - - archive.inbox=Archiv Posteingang archive.sent=Archiv Postausgang archive.activity=Archiv aller Aktionen archive.folder=Ordner Archive -archive.folder.name=Name +archive.folder.name=Gruppenname archive.folder.date=Datum archive.folder.messages=SMS archive.folder.none=Keine archivierten Ordner -archive.activity.name=Name +archive.activity.name=Gruppenname archive.activity.type=Typ archive.activity.date=Datum archive.activity.messages=SMS @@ -263,7 +206,6 @@ frontlinesms2.Autoreply.name.unique.error.frontlinesms2.Autoreply.name={1} {0} " frontlinesms2.Autoreply.name.validator.error.frontlinesms2.Autoreply.name=Ein Makro mit diesem Namen existiert bereits frontlinesms2.Keyword.value.validator.error.frontlinesms2.Autoreply.keyword.value=Das Schlüsselwort "{2}" gibt es schon frontlinesms2.Autoreply.autoreplyText.nullable.error.frontlinesms2.Autoreply.autoreplyText=Die SMS darf nicht leer sein. - contact.new=Neuer Kontakt contact.list.no.contact=Es bestehen noch keine Kontakte! contact.header=Kontakte @@ -277,31 +219,28 @@ contact.add.to.group=Gruppen zuordnen... contact.remove.from.group=Aus Gruppe entfernen contact.customfield.addmoreinformation=Zusäztliche Details... contact.customfield.option.createnew=Neues Feld... -contact.name.label=Name -contact.phonenumber.label=Handy -contact.phonenumber.international.warning=Die Telefonnummer entspricht nicht dem internationalem Format. Dies kann bei der Zuordnung von SMS und Kontakten zu Problemen führen. +contact.name.label=Gruppenname +contact.phonenumber.label=Telefonnummer contact.notes.label=Notizen -contact.email.label=Email +contact.email.label=E-Mail contact.groups.label=Gruppen contact.notinanygroup.label=Keiner Gruppe zugeordnet contact.messages.label=SMS -contact.sent.messages={0} SMS verschickt +contact.messages.sent={0} SMS verschickt contact.received.messages={0} SMS bekommen contact.search.messages=Nach SMS suchen - group.rename=Gruppe umbenennen group.edit=Gruppe bearbeiten group.delete=Gruppe löschen group.moreactions=Weitere Optionen... - customfield.validation.prompt=Bitte geben sie einen Namen ein. -customfield.name.label=Name +customfield.name.label=Gruppenname export.contact.info=Um die Kontakte zu exportieren, wählen Sie eine Exportart und die zu exportierenden Details. export.message.info=Um die SMS zu exportieren, wählen Sie die Exportart und die zu exportierenden Details. export.selectformat=Wählen sie ein Zielformat export.csv=CSV, bevorzugt für Tabellenkalkulationen (Excel) export.pdf=PDF, nützlich zum Drucken -folder.name.label=Name +folder.name.label=Gruppenname group.delete.prompt=Wollen sie die folgende Gruppe löschen {0}? ACHTUNG: Dies kann nicht widerrufen werden. layout.settings.header=Einstellungen activities.header=Aktivitäten @@ -323,8 +262,6 @@ poll.message.label=Umfragetext poll.choice.validation.error.deleting.response=Eine gespeicherte Wohl kann nicht leer sein poll.alias=Aliasse poll.aliases.prompt=Geben Sie Aliasse für die jeweiligen Optionen ein. -poll.aliases.prompt.details=Sie können mehrere Optionen mit Kommas trennen. Der erste Alias wird in der Umfrageanleitung genutzt. -poll.alias.validation.error=Aliasse müssen einzigartig sein poll.sort.label=Autosortierung poll.autosort.no.description=Antworten werden nicht automatisch sortiert. poll.autosort.description=Nach Schlüsselwort sortieren @@ -346,28 +283,24 @@ poll.response.enabled=Automatische Antwort ist aktiviert poll.message.edit=Bearbeiten Sie den Umfragetext poll.message.prompt=Dieser Umfragetext wird den Empfängern zugestellt poll.message.count=160 Zeichen verbleiben (1 SMS) - poll.moreactions.delete=Umfrage löschen poll.moreactions.rename=Umfrage umbenennen poll.moreactions.edit=Umfrage bearbeiten poll.moreactions.export=Umfrage exportieren - #TODO embed javascript values -poll.reply.text=Antworten Sie mit "{0} {1}" für Ja, "{2} {3}" für Nein. -poll.reply.text1={0} "{1} {2}" für {3} +poll.reply.text=Antworten Sie mit "{0}" für Ja, "{1}" für Nein. +poll.reply.text1={0} "{1}" für {2} poll.reply.text2=Bitte antworten Sie mit 'Ja' oder 'Nein' -poll.reply.text3= oder -poll.reply.text4={0} {1} +poll.reply.text3=oder poll.reply.text5=Antworten Sie poll.reply.text6=Bitte antworten Sie poll.message.send={0} {1} poll.recipients.validation.error=Wählen Sie die Empfänger -frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name= {1} {0} "{2}" muss eindeutig sein +frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name={1} {0} "{2}" muss eindeutig sein frontlinesms2.Poll.responses.validator.error.frontlinesms2.Poll.responses=Jede Antwort kann es nur einmal geben -frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value= Das Schlüsselwort "{2}" existiert schon - +frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value=Das Schlüsselwort "{2}" existiert schon wizard.title.new=Neu -wizard.fmessage.edit.title=Bearbeiten von {0} +wizard.fmessage.edit.title=Bearbeiten von {0} popup.title.saved={0} wurde gesichert! popup.activity.create=Wählen sie eine Aktion popup.smartgroup.create=Neue intelligente Gruppe @@ -386,14 +319,14 @@ smallpopup.messages.export.title=Ergebnisse exportieren ({0} SMS) smallpopup.test.message.title=Test-SMS smallpopup.recipients.title=Empfänger smallpopup.folder.title=Ordner -smallpopup.group.title=Gruppe -smallpopup.contact.export.title=Export +smallpopup.group.create.title=Group +smallpopup.contact.export.title=Exportieren smallpopup.contact.delete.title=Löschen contact.selected.many={0} Kontakte ausgewählt group.join.reply.message=Willkommen group.leave.reply.message=Auf Wiedersehen fmessage.new.info=Klicken sie hier um {0} neue SMS anzuzeigen. -wizard.quickmessage.title=Neue SMS +wizard.quickmessage.title=Send Message wizard.messages.replyall.title=Alle beantworten wizard.send.message.title=SMS senden wizard.ok=OK @@ -401,11 +334,8 @@ wizard.create=Neu wizard.send=Senden common.settings=Einstellungen common.help=Hilfe - activity.validation.prompt=Bitte füllen sie alle erforderlichen Felder aus autoreply.blank.keyword=Ohne ein Schlüsselwort werden alle eingehenden SMS von diesem Makro automatisch beantwortet. - - poll.type.prompt=Bitte wählen sie eine Umfrageart poll.question.yes.no='Ja' oder 'Nein' Frage poll.question.multiple='Multiple choice' Umfrage (z.B. 'Rot', 'Blau', 'Grün') @@ -422,12 +352,11 @@ poll.sort.automatically=Antworten mit dem folgenden Schlüsselwort automatisch e poll.validation.prompt=Bitte füllen sie alle erforderlichen Felder aus poll.name.validator.error.name=Es existiert bereits eine Umfrage mit diesem Titel pollResponse.value.blank.value=Umfrageantworten können nicht leer sein -poll.alias.validation.error.invalid.alias=Unzulässiges Alias. Versuchen sie einen Namen oder ein Wort poll.question=Umfragetext eingeben poll.response=Antwortliste poll.sort=Autmatisch einordnen poll.reply=Automatisch beantworten -poll.edit.message=Aufforderung bearbeiten +poll.edit.message=Aufforderung bearbeiten #edit message is better translated with Nachricht bearbeiten or Text bearbeiten!!! poll.recipients=Empfänger poll.confirm=Bestätigen @@ -435,7 +364,7 @@ poll.save=Die Umfrage wurde gesichert! poll.messages.queue=Falls gewünscht, sind Umfrageaufforderungen jetzt im Ordner 'Postausgang'. poll.messages.queue.status=Je nach Verbindungsqualität und Anzahl der SMS kann das Verschicken eine Weile dauern. poll.pending.messages=Um den aktuellen Status zu sehen, öffnen Sie den Ordner 'Postausgang'. -poll.send.messages.none=Keine Aufforderung +poll.send.messages.none=Keine Aufforderung #no messages will be sent is better translated with Keine Nachricht versendet quickmessage.details.label=Details bestätigen quickmessage.message.label=SMS @@ -455,8 +384,7 @@ quickmessage.phonenumber.label=Neue Telefonnummer: quickmessage.phonenumber.add=Hinzufügen quickmessage.selected.recipients=Empfänger ausgewählt quickmessage.validation.prompt=Bitte füllen sie alle erforderlichen Felder aus - -fmessage.number.error=Beim Sichern werden Zeichen aus diesem Feld entfernt +fmessage.number.error=Non-numeric characters in this field will be removed when saved search.filter.label=Suche einschränken auf search.filter.group=Gruppe auswählen search.filter.activities=Action/Ordner wählen @@ -466,7 +394,7 @@ search.filter.sent=Nur ausgegangene SMS search.filter.archive=Archive einbeziehen search.betweendates.label=Im Zeitraum search.header=Suche -search.quickmessage=SMS +search.quickmessage=Send message search.export=Resultate exportieren search.keyword.label=Schlüsselwort oder Begriff search.contact.name.label=Kontakt @@ -477,29 +405,22 @@ settings.general=Allgemein settings.connections=Telefone & Verbindungen settings.logs=System settings.general.header=Einstellungen > Allgemein -settings.logs.header=Systemlogs +settings.logs.header=Settings > System Logs logs.none=Sie haben keine Logs. logs.content=SMS logs.date=Zeit logs.filter.label=Logs der letzten logs.filter.anytime=Keine Einschränkung -logs.filter.1day=24 Stunden -logs.filter.3days=3 Tage -logs.filter.7days=7 Tage -logs.filter.14days=14 Tage -logs.filter.28days=28 Tage logs.download.label=Systemlogs herunterladen logs.download.buttontext=Herunterladen logs.download.title=Logs zum Versenden herunterladen logs.download.continue=Weiter - smartgroup.validation.prompt=Bitte füllen sie alle erforderlichen Felder aus und beachten Sie, dass nur ein Suchkriterium pro Feld zulässig ist. smartgroup.info=Wählen sie die Suchkriterien, nach denen die intelligente Gruppe erstellt werden soll. smartgroup.contains.label=enthält smartgroup.startswith.label=beginnt mit smartgroup.add.anotherrule=Weitere Suchkriterien hinzufügen smartgroup.name.label=Gruppenname - modem.port=Port modem.description=Beschreibung modem.locked=Gesperrt? @@ -513,30 +434,12 @@ traffic.all.folders.activities=Alle Aktionen/Ordner anzeigen traffic.sent=Gesendet traffic.received=Empfangen traffic.total=Gesamt - tab.message=SMS tab.archive=Archiv tab.contact=Kontakte tab.status=Status tab.search=Suche - help.info=In dieser Beta-Version gibt es keine eingebaute Hilfe. Bitte wenden Sie sich an die Benutzerforen. - -# IntelliSms Fconnection -intellismsfconnection.label=IntelliSms Account -intellismsfconnection.type.label=Typ -intellismsfconnection.name.label=Name -intellismsfconnection.username.label=Benutzername -intellismsfconnection.password.label=Passwort - -intellismsfconnection.send.label=Zum Senden nutzen -intellismsfconnection.receive.label=Zum Empfangen nutzen -intellismsfconnection.receiveProtocol.label=Protokoll -intellismsfconnection.serverName.label=Server Name -intellismsfconnection.serverPort.label=Server Port -intellismsfconnection.emailUserName.label=Benutzername -intellismsfconnection.emailPassword.label=Passwort - #Controllers contact.label=Kontakt(e) contact.edited.by.another.user=Dieser Kontakt wurde gleichzeitig von einem anderen Benutzer bearbeitet @@ -545,15 +448,13 @@ contact.exists.warn=Es existiert bereits ein Kontakt mit dieser Telefonnummer contact.view.duplicate=Existierenden Kontakt öffnen contact.addtogroup.error=Hinzufügen und Löschen in derselben Gruppe ist nicht möglich! contact.mobile.label=Handy -contact.email.label=Email fconnection.label=Fconnection fconnection.name=Fconnection -fconnection.unknown.type=Unbekannte Verbindungsart: +fconnection.unknown.type=Unbekannte Verbindungsart: fconnection.test.message.sent=Test-SMS wurde verschickt! announcement.saved=Die Mitteilung wurde gesichert und in den Postausgang gelegt. announcement.not.saved=Die Mitteilung konnte nicht gesichert werden. announcement.id.exist.not=Eine Mitteilung mit ID {0} konnte nicht gefunden werden -autoreply.saved=Das Makro wurde gesichert! autoreply.not.saved=Das Makro konnte nicht gesichert werde! report.creation.error=Beim Erstellen des Reports ist ein Fehler aufgetreten export.message.title=FrontlineSMS SMS-Export @@ -567,7 +468,7 @@ export.message.source.mobile=Handynummer des Absenders export.contact.title=FrontlineSMS Kontaktexport export.contact.name=Kontaktname export.contact.mobile=Handynummer -export.contact.email=Email +export.contact.email=E-Mail export.contact.notes=Notizen export.contact.groups=Gruppen export.messages.name1={0} {1} ({2} SMS) @@ -575,36 +476,28 @@ export.messages.name2={0} ({1} SMS) export.contacts.name1={0} Gruppe ({1} Kontakte) export.contacts.name2={0} intelligente Gruppe ({1} Kontakte) export.contacts.name3=Alle Kontakte ({0}) -folder.label=Ordner folder.archived.successfully=Ordner wurde erfolgreich archiviert! folder.unarchived.successfully=Ordner wurde erfolgreich wieder aktiviert! folder.trashed=Ordner wurde in den Papierkorb gelegt! folder.restored=Ordner wurde wiederhergestellt! folder.exist.not=Ein Ordner mit ID {0} konnte nicht gefunden werden folder.renamed=Order wurde umbenannt - group.label=Gruppe -group.name.label=Name +group.name.label=Gruppenname group.update.success=Die Gruppe wurde erfolgreich aktualisiert group.save.fail=Die Gruppe konnte nicht gesichert werden group.delete.fail=Die Gruppe konnte nicht gelöscht werden - -import.label=Importieren -import.backup.label=Daten von einem vorherigen Backup importieren -import.prompt.type=Zu importierende Daten auswählen -import.contacts=Kontaktdetails -import.messages=Nachrichtendetails -import.version1.info=Um von der ersten Version zu importieren, exportieren Sie die Daten bitte in Englisch -import.prompt=Zu importierende Datei auswählen +import.label=Import contacts and messages +import.backup.label=You can use the form below to import your contacts. You can also import messages that you have from a previous Frontline project. +import.prompt.type=Select the type of data you wish to import before uploading +import.messages=Messages in Frontline's CSV format +import.prompt=Select the file containing your data to initiate the import import.upload.failed=Datei konnte nicht hochgeladen werden. import.contact.save.error=Beim Sichern des Kontaktes ist ein Fehler aufgetreten. import.contact.complete={0} Kontakte wurden importiert; {1} sind fehlgeschlagen -import.contact.failed.download=Die Kontaktdaten (CSV) konnten nicht geladen werden import.message.save.error=Beim Sichern der SMS ist eine Fehler aufgetreten import.message.complete={0} SMS wurden importiert; {1} fehlgeschlagen - -many.selected= {0} {1} ausgewählt - +many.selected={0} {1} ausgewählt flash.message.activity.found.not=Die Aktion wurde nicht gefunden flash.message.folder.found.not=Der Ordner wurde nicht gefunden flash.message=SMS @@ -613,7 +506,6 @@ flash.message.fmessages.many={0} SMS flash.message.fmessages.many.one=1 SMS fmessage.exist.not=Eine SMS mit ID {0} konnte nicht gefunden werden flash.message.poll.queued=Die Umfrage wurde gesichert und die Nachrichten in den Postausgang gelegt -flash.message.poll.saved=Die Umfrage wurde gesichert flash.message.poll.not.saved=Die Umfrage konnte nicht gesichert werden! system.notification.ok=OK system.notification.fail=Fehler @@ -622,53 +514,47 @@ flash.smartgroup.saved=Die intelligente Gruppe {0} wurde gesichert flash.smartgroup.save.failed=Beim Sichern der intelligenten Gruppe sind die folgenden Fehler aufgetreten {0} smartgroup.id.exist.not=Eine intelligente Gruppe mit ID {0} konnte nicht gefunden werden smartgroup.save.failed=Beim Sichern der intelligente Gruppe {0} mit den folgenden Suchkriterien {1}{2} ist ein Fehler aufgetreten: {3} -contact.name.label=Name -contact.phonenumber.label=Telefonnummer - searchdescriptor.searching=Suche searchdescriptor.all.messages=alle SMS -searchdescriptor.archived.messages= (einschliesslich archivierter SMS) -searchdescriptor.exclude.archived.messages= (ohne archivierte SMS) +searchdescriptor.archived.messages=(einschliesslich archivierter SMS) +searchdescriptor.exclude.archived.messages=(ohne archivierte SMS) searchdescriptor.only=, nur {0} searchdescriptor.between=, zwischen {0} und {1} searchdescriptor.from=, von {0} searchdescriptor.until=, bis {0} -poll.title={0} Umfrage -announcement.title={0} Mitteilung -autoreply.title={0} Automatische Antwort -folder.title={0} Ordner +poll.title={0} +announcement.title={0} +autoreply.title={0} +folder.title={0} frontlinesms.welcome=Willkommen bei FrontlineSMS! \\o/ failed.pending.fmessages={0} SMS konnten nicht gesendet werden. Siehe 'Postausgang'. - language.label=Sprache language.prompt=Hier können Sie die Sprache der FrontlineSMS Benutzeroberfläche ändern frontlinesms.user.support=FrontlineSMS Benutzerhilfe download.logs.info1=Achtung: Das FrontlineSMS-Team kann nicht direkt auf gesendete Logs reagieren. Falls Sie Hilfe benötigen, suchen Sie bitte in den Hilfsdokumenten nach einer Antwort, oder stellen Sie Ihre Frage in den Benutzerforen. download.logs.info2=Andere Benutzer haben möglicherweise schon eine Lösung zu einem ähnliches Problem gefunden. Andernfalls klicken Sie 'Weiter' um die Systemlogs weiterzuleiten. - dynamicfield.contact_name.label=Kontaktname dynamicfield.contact_number.label=Kontaktnummer dynamicfield.keyword.label=Schlüsselwort dynamicfield.message_content.label=Inhalt - -# Fmessage domain +# TextMessage domain fmessage.queued=Die SMS an {0} wurde in den Postausgang gelegt fmessage.queued.multiple=Die SMS an {0} Empfänger wurde in den Postausgang gelegt fmessage.retry.success=Die SMS an {0} wurde erneut in den Postausgang gelegt fmessage.retry.success.multiple={0} SMS wurden erneut in den Postausgang gelegt -fmessage.displayName.label=Name +fmessage.displayName.label=Gruppenname fmessage.text.label=Inhalt fmessage.date.label=Datum fmessage.to=An: {0} fmessage.to.multiple=An: {0} Empfänger -fmessage.quickmessage=Kurzmitteilung +fmessage.quickmessage=Send message fmessage.archive=Archivieren fmessage.activity.archive={0} archivieren -fmessage.unarchive={0} aktivieren +fmessage.unarchive=Aktivieren fmessage.export=Exportieren fmessage.rename={0} umbenennen fmessage.edit={0} bearbeiten -fmessage.delete={0} löschen +fmessage.delete=Delete fmessage.moreactions=Weiter optionen... fmessage.footer.show=Anzeigen fmessage.footer.show.failed=Fehlgeschlagen @@ -687,37 +573,31 @@ fmessage.resend=Erneut verschicken fmessage.retry=Erneut versuchen fmessage.reply=Antworten fmessage.forward=Weiterleiten -fmessage.unarchive=Aktivieren -fmessage.delete=Löschen fmessage.messages.none=Keine SMS fmessage.selected.none=Keine SMS ausgewählt fmessage.move.to.header=SMS verschieben... fmessage.move.to.inbox=Eingang -fmessage.archive.many=Alle archivieren +fmessage.archive.many=Archive selected fmessage.count=1 SMS fmessage.count.many={0} SMS -fmessage.many= SMS -fmessage.delete.many=Alle löschen -fmessage.reply.many=Alle beantworten +fmessage.many=SMS +fmessage.delete.many=Delete selected +fmessage.reply.many=Reply selected fmessage.restore=Wiederherstellen fmessage.restore.many=Wiederherstellen -fmessage.retry.many=Versuche sind fehlgeschlagen +fmessage.retry.many=Retry selected fmessage.selected.many={0} SMS ausgewählt -fmessage.unarchive.many=Alle aktivieren - +fmessage.unarchive.many=Unarchive selected # TODO move to poll.* fmessage.showpolldetails=Diagramm anzeigen fmessage.hidepolldetails=Diagramm ausblenden - # TODO move to search.* fmessage.search.none=Es wurden keine SMS gefunden fmessage.search.description=Beginnen Sie eine neue Suche auf der linken Seite - -activity.name=Name +activity.name=Gruppenname activity.delete.prompt={0} in den Papierkorb legen; inklusive aller assoziierten SMS activity.label=Aktion activity.categorize=Antwort kategorisieren - magicwand.title=Substitutionformel einfügen folder.create.success=Ordner wurde erfolgreich erstellt folder.create.failed=Ordner konnte nicht erstellt werden @@ -731,7 +611,6 @@ announcement.name.blank.error=Der Mitteilungstitel kann nicht leer sein announcement.name.validator.error=Es existiert bereits eine Mitteilung mit diesem Titel group.name.blank.error=Der Gruppenname kann nicht leer sein group.name.validator.error=Es existiert bereits eine Gruppe mit diesem Namen - #Jquery Validation messages jquery.validation.required=Dieses Feld ist erforderlich. jquery.validation.remote=Bitte korrigieren Sie dieses Feld. @@ -750,4 +629,3 @@ jquery.validation.rangelength=Ihre Eingabe sollte {0} bis {1} Zeichen lang sein. jquery.validation.range=Wählen Sie bitte einen Wert zwischen {0} und {1}. jquery.validation.max=Bitte einen Wert von maximal {0} eingeben. jquery.validation.min=Bitte einen Wert von mindestens {0} eingeben. - diff --git a/plugins/frontlinesms-core/grails-app/i18n/messages_es.properties b/plugins/frontlinesms-core/grails-app/i18n/messages_es.properties index a16a9d1c5..17590d545 100644 --- a/plugins/frontlinesms-core/grails-app/i18n/messages_es.properties +++ b/plugins/frontlinesms-core/grails-app/i18n/messages_es.properties @@ -1,11 +1,9 @@ # FrontlineSMS English translation by the FrontlineSMS team, Nairobi language.name=Español - # General info app.version.label=Versión - # Common action imperatives - to be used for button labels and similar -action.ok=OK +action.ok=Aceptar action.close=Cerrar action.cancel=Cancelar action.done=Finalizar @@ -16,68 +14,78 @@ action.create=Crear action.edit=Editar action.rename=Renombrar action.save=Guardar -action.save.all=Guardar Todo +action.save.all=Save Selected action.delete=Borrar -action.delete.all=Borrar Todo +action.delete.all=Delete Selected action.send=Enviar action.export=Exportar - +action.view=Ver +content.loading=Cargando... # Messages when FrontlineSMS server connection is lost server.connection.fail.title=Se perdió la conexión con el servidor. server.connection.fail.info=Por favor reinicie FrontlineSMS, o cierre esta ventana. - #Connections: connection.creation.failed=No fue posible crear la conexión {0} -connection.route.destroyed=Ruta destruida desde {0} a {1} -connection.route.connecting=Conectando... -connection.route.disconnecting=Desconectando... +connection.route.disabled=Ruta destruida desde {0} a {1} connection.route.successNotification=Ruta creada exitosamente en {0} -connection.route.failNotification=No fue posible crear la ruta en {1}: {2} [edit] -connection.route.destroyNotification=Ruta desconectada en {0} +connection.route.failNotification=Failed to create connection on {1}: {2} [[[edit]](({0}))]. +connection.route.disableNotification=Ruta desconectada en {0} +connection.route.pauseNotification=Conexión Pausada en {0} +connection.route.resumeNotification=Conexión reaunadad en {0} connection.test.sent=El mensaje de prueba se envió satisfactoriamente a {0} utilizando {1} +connection.route.exception={1} # Connection exception messages connection.error.org.smslib.alreadyconnectedexception=Este dispositivo ya está conectado connection.error.org.smslib.gsmnetworkregistrationexception=Falló el registro con la red GSM connection.error.org.smslib.invalidpinexception=Proporcionó el PIN incorrecto connection.error.org.smslib.nopinexception=Se requiere el PIN. Favor de proporcionarlo. +connection.error.org.smslib.notconnectedexception={0} +connection.error.org.smslib.nosuchportexception=Port not found, or not accessible connection.error.java.io.ioexception=El puerto envió un error: {0} - -connection.header=Conexiones +connection.error.frontlinesms2.camel.exception.invalidapiidexception={0} +connection.error.frontlinesms2.camel.exception.authenticationexception={0} +connection.error.frontlinesms2.camel.exception.insufficientcreditexception={0} +connection.error.serial.nosuchportexception=Puerto no encontrado +connection.error.org.apache.camel.runtimecamelexception=No se puede establecer a la conexión +connection.error.onsave={0} +connection.header=Settings > Connections connection.list.none=No tiene conexiones configuradas. connection.edit=Editar Conexión connection.delete=Borrar Conexión connection.deleted=Se borró la conexión {0} . -connection.route.create=Crear ruta +connection.route.enable=Habilitar +connection.route.retryconnection=Reintentar connection.add=Agregar nueva conexión connection.createtest.message.label=Mensaje -connection.route.destroy=Destruir ruta +connection.route.disable=Destruir ruta connection.send.test.message=Enviar mensaje de prueba connection.test.message=FrontlineSMS lo felicita \\o/ ha logrado configurar {0} exitosamente para enviar SMS \\o/ -connection.validation.prompt=Por favor llene todos los campos requeridos +connection.validation.prompt=Por favor llene todos los campos obligatorios connection.select=Seleccione el tipo de conexión connection.type=Seleccione el tipo connection.details=Ingrese los detalles connection.confirm=Confirme connection.createtest.number=Número connection.confirm.header=Confirme la configuración -connection.name.autoconfigured= {0} {1} auto-configurado en puerto {2}" - -status.connection.header=Conexiones +connection.name.autoconfigured={0} {1} auto-configurado en puerto {2}" +status.connection.title=Conexiones +status.connection.manage=Administrar sus conexiones status.connection.none=No tiene conexiones configuradas. status.devises.header=Dispositivos detectados status.detect.modems=Modems detectados status.modems.none=Aún no se detectan dispositivos. - -connectionstatus.not_connected=Sin conexión +status.header=Usage Statistics connectionstatus.connecting=Conectando connectionstatus.connected=Conectado - +connectionstatus.disabled=Deshabilitado +connectionstatus.failed=Falló +connectionstatus.not_connected=Sin conexión default.doesnt.match.message=La propiedad [{0}] de clase [{1}] con valor [{2}] no coincide con el patrón requerido [{3}] default.invalid.url.message=La propiedad [{0}] de clase [{1}] con valor [{2}] no es un hipervínculo válido default.invalid.creditCard.message=La propiedad [{0}] de clase [{1}] con valor [{2}] no es un número de tarjeta de crédito válido default.invalid.email.message=La propiedad [{0}] de clase [{1}] con valor [{2}] no es una dirección de correo electrónico válida -default.invalid.range.message=La propiedad [{0}] de clase [{1}] con valor [{2}] no cae dentro del rango válido de [{3}] para [{4}] -default.invalid.size.message=La propiedad [{0}] de clase [{1}] con valor [{2}] no cae dentro del rango de tamaño válido de [{3}] para [{4}] +default.invalid.range.message=La propiedad [{0}] de clase [{1}] con valor [{2}] no cae dentro del rango válido de [{3}] a [{4}] +default.invalid.size.message=La propiedad [{0}] de clase [{1}] con valor [{2}] no cae dentro del rango de tamaño válido de [{3}] a [{4}] default.invalid.max.message=La propiedad [{0}] de clase [{1}] con valor [{2}] excede el valor máximo [{3}] default.invalid.min.message=La propiedad [{0}] de clase [{1}] con valor [{2}] es menor que el valor mínimo [{3}] default.invalid.max.size.message=La propiedad [{0}] de clase [{1}] con valor [{2}] excede el tamaño máximo de [{3}] @@ -88,39 +96,33 @@ default.blank.message=La propiedad [{0}] de clase [{1}] no puede quedar en blanc default.not.equal.message=La propiedad [{0}] de clase [{1}] con valor [{2}] no puede ser igual a [{3}] default.null.message=La propiedad [{0}] de clase [{1}] no puede ser nula default.not.unique.message=La propiedad [{0}] de clase [{1}] con valor [{2}] debe ser única - default.paginate.prev=Anterior default.paginate.next=Siguiente default.boolean.true=Verdadero default.boolean.false=Falso -default.date.format=dd MMMM, yyyy hh:mm +default.date.format=dd MMMM, aaaa hh:mm default.number.format=0 - default.unarchived={0} Extraído -default.unarchive.failed=Fallo al extraer {0} -default.trashed={0} colocado en papelera +default.unarchive.failed=Falló la extracción {0} default.restored={0} restaurado default.restore.failed=No fue posible restaurar {0} con identificación {1} -default.archived={0} archivado exitosamente! default.archived.multiple={0} archivado default.created={0} creado default.created.message={0} {1} ha sido creado -default.create.failed=Fallo al crear {0} -default.updated=Se actualizó {0} +default.create.failed=Falló la creación {0} +default.updated=Se actualizó {0} default.update.failed=Falló la actualización de {0} con identificación {1} -default.updated.multiple= {0} se han actualizado +default.updated.multiple={0} se han actualizado default.updated.message={0} actualizado default.deleted={0} eliminado -default.trashed={0} colocado en papelera +default.trashed={0} moved to trash default.trashed.multiple={0} colocados en papelera -default.archived={0} archivado -default.unarchived={0} extraído -default.unarchive.keyword.failed=Fallo al extraer {0}. La palabra clave ya había sido utilizada. +default.archived={0} archived +default.unarchive.keyword.failed=Fallo al extraer {0}. La palabra clave ya ha sido utilizada default.unarchived.multiple={0} extraído default.delete.failed=No fue posible eliminar {0} con identificación {1} default.notfound=No fue posible encontrar {0} con identificación {1} default.optimistic.locking.failure=Otro usuario ha actualizado este {0} mientras usted estaba editando - default.home.label=Inicio default.list.label={0} Lista default.add.label=Agregar {0} @@ -129,81 +131,117 @@ default.create.label=Crear {0} default.show.label=Mostrar {0} default.edit.label=Editar {0} search.clear=Limpiar búsqueda - default.button.create.label=Crear default.button.edit.label=Editar default.button.update.label=Actualizar default.button.delete.label=Eliminar default.button.search.label=Buscar default.button.apply.label=Aplicar -default.button.delete.confirm.message=Está seguro? - +default.button.delete.confirm.message=¿Está seguro? default.deleted.message={0} eliminado - # Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) typeMismatch.java.net.URL=La propiedad {0} debe ser un hipervínculo válido typeMismatch.java.net.URI=La propiedad {0} debe ser un 'localizador uniforme de recursos' válido -typeMismatch.java.util.Date=La propiedad {0} debe ser una Fecha válida +typeMismatch.java.util.Date=La propiedad {0} debe ser una fecha válida typeMismatch.java.lang.Double=La propiedad {0} debe ser un número válido typeMismatch.java.lang.Integer=La propiedad {0} debe ser un número válido typeMismatch.java.lang.Long=La propiedad {0} debe ser un número válido typeMismatch.java.lang.Short=La propiedad {0} debe ser un número válido typeMismatch.java.math.BigDecimal=La propiedad {0} debe ser un número válido typeMismatch.java.math.BigInteger=La propiedad {0} debe ser un número válido -typeMismatch.int = {0} debe ser un número válido - +typeMismatch.int={0} debe ser un número válido # Application specific messages -messages.trash.confirmation=Esto vaciará la papelera y eliminará los mensajes permanentemente. Desea continuar? +messages.trash.confirmation=Esto vaciará la papelera y eliminará los mensajes permanentemente. ¿Desea continuar? default.created.poll=Se ha creado la encuesta! default.search.label=Limpiar búsqueda -default.search.betweendates.title=Entre las fechas: +default.search.betweendates.title=Ingrese las fechas: default.search.moresearchoption.label=Más opciones de búsqueda -default.search.date.format=d/M/yyyy +default.search.date.format=d/M/aaaa default.search.moreoption.label=Más opciones - # SMSLib Fconnection -smslibfconnection.label=Teléfono/Modem -smslibfconnection.type.label=Tipo -smslibfconnection.name.label=Nombre -smslibfconnection.port.label=Puerto -smslibfconnection.baud.label=Velocidad de transmisión en baudios -smslibfconnection.pin.label=PIN -smslibfconnection.imsi.label=SIM IMSI -smslibfconnection.serial.label=Número de serie del dispositivo -smslibfconnection.send.label=Utilizar el modem ÚNICAMENTE para enviar mensajes -smslibfconnection.receive.label =Utilizar el modem ÚNICAMENTE para recibir mensajes -smslibFconnection.send.validator.error.send=El Modem debe utilizarse para enviar -smslibFconnection.receive.validator.error.receive=o recibir mensajes - +smslib.label=Teléfono/Modem +smslib.type.label=Tipo +smslib.name.label=Nombre +smslib.manufacturer.label=fabricante +smslib.model.label=modelo +smslib.port.label=Puerto +smslib.baud.label=Velocidad de transmisión en baudios +smslib.pin.label=PIN +smslib.imsi.label=SIM IMSI +smslib.serial.label=Número de serie del dispositivo +smslib.sendEnabled.label=Usar para enviar +smslib.receiveEnabled.label=Usar para recibir +smslibFconnection.sendEnabled.validator.error.send=El modemo se debe usar para enviar +smslibFconnection.receiveEnabled.validator.error.receive=o recibir mensajes +smslib.description=Conectar a USB, módems seriales y bluetooth o teléfonos +smslib.global.info=FrontlineSMS intentará configurar automáticamente cualquier módem o teléfono conectados, pero puede configurarlo de forma manual aquí # Email Fconnection -emailfconnection.label=Correo electrónico -emailfconnection.type.label=Tipo -emailfconnection.name.label=Nombre -emailfconnection.receiveProtocol.label=Protocolo -emailfconnection.serverName.label=Nombre del Servidor -emailfconnection.serverPort.label=Puerto del Servidor -emailfconnection.username.label=Usuario -emailfconnection.password.label=Contraseña - +email.label=Correo electrónico +email.type.label=Tipo +email.name.label=Nombre +email.receiveProtocol.label=Protocolo +email.serverName.label=Nombre del Servidor +email.serverPort.label=Puerto del Servidor +email.username.label=Usuario +email.password.label=Contraseña # CLickatell Fconnection -clickatellfconnection.label=Cuenta de Clickatell -clickatellfconnection.type.label=Tipo -clickatellfconnection.name.label=Nombre -clickatellfconnection.apiId.label=API ID -clickatellfconnection.username.label=Usuario -clickatellfconnection.password.label=Contraseña - +clickatell.label=Cuenta de Clickatell +clickatell.type.label=Tipo +clickatell.name.label=Nombre +clickatell.apiId.label=ID de la API +clickatell.username.label=Usuario +clickatell.password.label=Contraseña +clickatell.sendToUsa.label=Enviar a EE.UU. +clickatell.fromNumber.label=Desde Número +clickatell.description=Enviar y recibir mensajes a través de una cuenta de Clickatell +clickatell.global.info=Usted tendrá que configurar una cuenta con Clickatell ( www.clickatell.com ) +clickatellFconnection.fromNumber.validator.invalid=Un 'Desde Número' es requerido para el envío de mensajes a los Estados Unidos +# TODO: Change markup below to markdown +clickatell.info-local=Para poder configurar una conexión Clickatell, debe primero tener una cuenta Clickatell. Si no tiene una por favor vaya al sitio de Clickatell y regístrese para obtener una 'Cuenta Central de Desarrollador'. Iniciar sesión para enviar mensajes es gratis, y el prceso toma menos de 5 minutos. Una vez que tenga una cuenta Clickatell activa, tendrá que 'Crear una Conexión (API ID)' desde la página principal. PRimero, elija 'APIs,', después 'Configurar una nueva API'. Desde ahí, elija 'Agregar API HTTP' con las configuraciones por defecto, después introduzca los detalles relevantes abajo. El campo de 'Nombre' es sólo para referencia de usted para su cuenta de Frontline y no está relacionado con la API de Clickatell, por ej.: 'Mi conexión de mensaje local'. +clickatell.info-clickatell=Los siguientes detalles se deben copiar y pegar directamente desde la pantalla de la API de Clickatell. +#Nexmo Fconnection +nexmo.label=Nexmo +nexmo.type.label=Conexión de Nexmo +nexmo.name.label=Nombre +nexmo.api_key.label=Clave API: +nexmo.api_secret.label=Secreto de la API +nexmo.fromNumber.label=Del número +nexmo.description=Enviar y recibir mensajes por medio de una cuenta Nexmo. +nexmo.receiveEnabled.label=Recepción habilitada +nexmo.sendEnabled.label=Envío habilitado +# Smssync Fconnection +smssync.label=SMSSync +smssync.name.label=Nombre +smssync.type.label=Tipo +smssync.receiveEnabled.label=Recibir habilitado +smssync.sendEnabled.label=Enviar habilitado +smssync.secret.label=Secreto +smssync.timeout.label=Timeout (minutos) +smssync.description=Utilice un teléfono Android con la aplicación Smssync instalada para enviar y recibir SMS con FrontlineSMS +smssync.field.secret.info=En su aplicación, establezca el secreto para que coincida con este campo +smssync.global.info=Descarga la aplicación SMSSync de smssync.ushahidi.com +smssync.timeout=El teléfono Android asociado con"{0}" no ha contactad su cuenta Frontline para {1} minuto(s) [] +smssync.info-setup=Ls products de Frontline le permiten a usted enviar y recibir mensajes por medio de su teléfono Android. Para poder hacer esto usted debe:\n\n1. Introducir un "Secreto" y dar un nombre a su conexión. Un secreto es simplemente una contraseña de su elección\n2. Descargue e instale [SMSSync de la tienda de Apps de Android](https://play.google.com/store/apps/details?id=org.addhen.smssync&hl=en) a su teléfono Android\n3. Una vez que haya creado esta conexión, puede crear una nueva Sync URL dentro de SMSSync en su teléfono Android introduciendo la conexión URL (generada por su producto Frontline y mostrado en la siguiente página) y escoja un secreto. Vaya a [el sitio de SMSSync](http://smssync.ushahidi.com/howto) para obtener más ayuda. +smssync.info-timeout=Si SMSSync no se contacta con su producto Frontline para una cierta duración (60 minutos por defecto), sus mensajes en cola NO se enviarán y usted verá una notificación de que los mensajes no se pudieron enviar. Seleccione esta duración abajo. +smssync.info-name=Finally, you should name your SMSSync connection with a name of your choice, e.g. 'Bob's work Android'. # Messages Tab message.create.prompt=Introducir mensaje message.character.count={0} caracteres restantes ({1} mensaje(s)SMS) message.character.count.warning=Puede ser más largo después de realizar sustituciones +message.header.inbox=Bandeja de entrada +message.header.sent=Enviado +message.header.pending=Pendiente +message.header.trash=Papelera +message.header.folder=Carpetas +message.header.activityList=ActivityList +message.header.folderList=FolderList announcement.label=Anuncio announcement.description=Envía un mensaje de anuncio y organiza las respuestas announcement.info1=El anuncio se ha guardado y el mensaje se ha agregado a la fila de mensajes pendientes. -announcement.info2=El envío de todos los mensajes puede demorar, dependiendo en el número de mensajes y en la conexión de red. -announcement.info3=Para ver el estado de sus mensajes, abra el folder de mensajes 'Pendientes'. +announcement.info2=El envío de todos los mensajes puede demorar, dependiendo del número de mensajes y de la conexión de red. +announcement.info3=Para ver el estado de sus mensajes, abra la carpeta de mensajes 'Pendientes'. announcement.info4=Para ver el anuncio, selecciónelo haciendo click en el menú de la izquierda. -announcement.validation.prompt=Por favor llene todos los campos requeridos. +announcement.validation.prompt=Por favor llene todos los campos obligatorios. announcement.select.recipients=Seleccione los destinatarios announcement.confirm=Confirme announcement.delete.warn=Eliminar {0} ADVERTENCIA: Esto no se puede revertir! @@ -216,14 +254,12 @@ announcement.recipients.label=Destinatarios announcement.create.message=Crear mensaje #TODO embed javascript values announcement.recipients.count=contactos seleccionados -announcement.messages.count=mensajes serán enviados +announcement.messages.count=Los mensajes serán enviados announcement.moreactions.delete=Eliminar anuncio announcement.moreactions.rename=Renombrar anuncio announcement.moreactions.edit=Editar anuncio announcement.moreactions.export=Exportar anuncio frontlinesms2.Announcement.name.unique.error.frontlinesms2.Announcement.name={1} {0} "{2}" debe ser único - - archive.inbox=Comprimir bandeja de entrada archive.sent=Listado de mensajes enviados archive.activity=Listado de actividades completadas @@ -236,19 +272,22 @@ archive.activity.name=Nombre archive.activity.type=Tipo archive.activity.date=Fecha archive.activity.messages=Mensajes -archive.activity.list.none=  no hay actividades archivadas +archive.activity.list.none=  No hay actividades archivadas +archive.header=Archivo autoreply.enter.keyword=Ingresar palabra clave autoreply.create.message=Ingresar mensaje +activity.autoreply.sort.description=Si los usuarios envian mensajes que comienzan con una determinada palabra clave, FrontlineSMS automáticamente puede procesar los mensajes en el sistema. +activity.autoreply.disable.sorting.description=Los mensajes no se moverán a esta actividad o respondió de forma automática. autoreply.confirm=Confirmar autoreply.name.label=Mensaje autoreply.details.label=Confirmar información autoreply.label=Respuesta Automática autoreply.keyword.label=Palabra clave autoreply.description=Responder automáticamente a mensajes entrantes -autoreply.info=Ha creado una respuesta automática. Cualquier mensaje que contenga la palabra clave definida por usted recibirá la respuesta automática. Para revisar la actividad de respuesta automática, haga click en el menú del lado derecho. -autoreply.info.warning=Las Respuestas Automáticas sin palabra clave se utilizarán para responder a todos los mensajes entrantes. +autoreply.info=Ha creado una respuesta automática. Cualquier mensaje que contenga la palabra clave definida por usted recibirá una respuesta automática. Para revisar el contenido de la respuesta automática, haga click en el menú del lado derecho. +autoreply.info.warning=Las respuestas automáticas sin palabra clave se utilizarán para responder a todos los mensajes entrantes. autoreply.info.note=Aviso: Si archiva la respuesta automática, esta dejará de enviarse para responder a mensajes entrantes. -autoreply.validation.prompt=Por favor llene todos los campos requeridos. +autoreply.validation.prompt=Por favor llene todos los campos obligatorios. autoreply.message.title=Mensaje que se devolverá al recibir esta respuesta automática: autoreply.keyword.title=Ordenar mensaje automáticamente usando una palabra clave: autoreply.name.prompt=Nombre esta respuesta automática @@ -257,18 +296,42 @@ autoreply.moreactions.delete=Eliminar respuesta automática autoreply.moreactions.rename=Renombrar respuesta automática autoreply.moreactions.edit=Editar respuesta automática autoreply.moreactions.export=Exportar respuesta automática -autoreply.all.messages=No utilizar palabra clave (TODOS los mensajes entrantes recibirán esta respuesta automática) +autoreply.all.messages=No utilizar palabra clave (Todos los mensajes entrantes recibirán esta respuesta automática). autoreply.text.none=Ninguno frontlinesms2.Autoreply.name.unique.error.frontlinesms2.Autoreply.name={1} {0} "{2}" debe ser único frontlinesms2.Autoreply.name.validator.error.frontlinesms2.Autoreply.name=El nombre de la respuesta automática debe ser único frontlinesms2.Keyword.value.validator.error.frontlinesms2.Autoreply.keyword.value=La palabra clave "{2}" ya ha sido utilizada frontlinesms2.Autoreply.autoreplyText.nullable.error.frontlinesms2.Autoreply.autoreplyText=El mensaje no puede estar en blanco - +autoforward.title={0} +autoforward.label=Auto enviar +autoforward.description=Reenviar automáticamente los mensajes entrantes a contactos +autoforward.recipientcount.current=Actualmente {0} destinatarios +autoforward.create.message=Ingrese mensaje +autoforward.confirm=Confirmar +autoforward.recipients=Destinatarios +autoforward.name.prompt=Nombre este Autoforward +autoforward.details.label=Confirmar los detalles +autoforward.keyword.label=Palabra(s) clave +autoforward.name.label=Mensaje +autoforward.contacts=Contactos +autoforward.groups=Grupos +autoforward.info=El Autoforward se ha creado, todos los mensajes que contengan la palabra clave serán agregados a esta actividad de Autoforward que se puede ver haciendo clic sobre ella en el menú de la derecha. +autoforward.info.warning=Un Autoforward sin una palabra clave resultará en que todos los mensajes entrantes se reenvíen +autoforward.info.note=Nota: Si archiva el Autoforward, los mensajes entrantes ya no serán ordenados por él. +autoforward.save=El autoforward se ha guardado! +autoforward.save.success={0} Autoforward se ha guardado! +autoforward.global.keyword=Ninguno (todos los mensajes entrantes serán procesados) +autoforward.disabled.keyword=Ninguno (clasificación automática desactivada) +autoforward.keyword.none.generic=Ninguno +autoforward.groups.none=Ninguno +autoforward.contacts.none=Ninguno +autoforward.message.format=Mensaje contact.new=Contacto Nuevo contact.list.no.contact=Aquí no hay contactos! contact.header=Contactos +contact.header.group=Contactos >> {0} contact.all.contacts=Todos los contactos -contact.create=Crear contacto nuevo +contact.create=Crear contacto nuevo contact.groups.header=Grupos contact.create.group=Crear grupo nuevo contact.smartgroup.header=Grupos inteligentes @@ -278,23 +341,30 @@ contact.remove.from.group=Eliminar del grupo contact.customfield.addmoreinformation=Agregar más información... contact.customfield.option.createnew=Crear nuevo... contact.name.label=Nombre -contact.phonenumber.label=Número móbil o celular -contact.phonenumber.international.warning=Este número no está en formato internacional. Esto puede causar problemas al relacionar los mensajes con sus contactos. +contact.phonenumber.label=Número móvil o celular contact.notes.label=Notas contact.email.label=Correo electrónico contact.groups.label=Grupos contact.notinanygroup.label=No es parte de ningún grupo contact.messages.label=Mensajes -contact.sent.messages={0} mensajes enviados +contact.messages.sent={0} mensajes enviados contact.received.messages={0} mensajes recibidos contact.search.messages=Buscar mensajes - +contact.select.all=Seleccionar todo +contact.search.placeholder=Search your contacts, or enter phone numbers +contact.search.contact=Contactos +contact.search.smartgroup=Grupos inteligentes +contact.search.group=Grupos +contact.search.address=Agregar un número telefónico: +contact.not.found=Contacto no encontrado +group.not.found=Grupo no encontrado +smartgroup.not.found=Grupo inteligente no encontrado group.rename=Renombrar grupo group.edit=Editar grupo group.delete=Eliminar grupo group.moreactions=Más acciones... - customfield.validation.prompt=Por favor ingrese un nombre +customfield.validation.error=Ese nombre ya existe customfield.name.label=Nombre export.contact.info=Para exportar contactos desde FrontlineSMS, seleccione el tipo de exportación y la información que se debe de incluir en los datos exportados. export.message.info=Para exportar contactos desde FrontlineSMS, seleccione el tipo de exportación y la información que se debe de incluir en los datos exportados. @@ -320,20 +390,35 @@ fmessage.label.multiple={0} mensajes poll.prompt=Nombre esta encuesta poll.details.label=Confirme la información poll.message.label=Mensaje -poll.choice.validation.error.deleting.response=Una opción guardada no puede tener un valor vacío +poll.choice.validation.error.deleting.response=Una opción guardada no puede tener un valor en blanco poll.alias=Alias +poll.keywords=Palabras clave poll.aliases.prompt=Ingrese los alias para las opciones correspondientes. -poll.aliases.prompt.details=Puede ingresar múltiples alias para cada opción separados por comas. El primer alias será enviado en el mensaje de instrucciones de la encuesta. -poll.alias.validation.error=Los alias deben ser únicos +poll.keywords.prompt.details=La palabra clave de nivel superior será el nombre de la encuesta y se enviará en el mensaje de la encuesta instrucciones. Cada respuesta puede tener también otras palabras clave de acceso directo. +poll.keywords.prompt.more.details=Puede introducir varias palabras clave separadas por comas para el nivel superior y respuestas. Si no hay palabras clave de alto nivel introducido por debajo, entonces estas palabras clave de respuesta deben ser únicos en todas las actividades. +poll.keywords.response.label=Palabras clave de respuesta +poll.response.keyword=Establecer palabras claves de respuesta +poll.set.keyword=Establecer una palabra clave de nivel superior +poll.keywords.validation.error=Las palabras clave deben ser únicas poll.sort.label=Clasificar automáticamente -poll.autosort.no.description=Los mensajes no se clasificarán automaticamente. -poll.autosort.description=Clasificar mensajes con la palabra clave +poll.autosort.no.description=No clasificar automáticamente los mensajes. +poll.autosort.description=Clasificar los mensajes automáticamente. poll.sort.keyword=palabra clave -poll.sort.by=Clasificar por +poll.sort.toplevel.keyword.label=Palabra(s) clave de nivel superior (opcional) +poll.sort.by=Clasificar por poll.autoreply.label=Respuesta automática poll.autoreply.none=ninguno poll.recipients.label=Destinatarios poll.recipients.none=Ninguno +poll.toplevelkeyword=Palabras clave de nivel superior +poll.sort.example.toplevel=por ejemplo EQUIPO +poll.sort.example.keywords.A=por ejemplo A, ASOMBROSO +poll.sort.example.keywords.B=por ejemplo B, BELLA +poll.sort.example.keywords.C=por ejemplo C, CALIENTE +poll.sort.example.keywords.D=por ejemplo D, DELICIOSO +poll.sort.example.keywords.E=por ejemplo E, EJEMPLARES +poll.sort.example.keywords.yn.A=por ejemplo SI, AFIRMATIVO +poll.sort.example.keywords.yn.B=por ejemplo NO, NEGATIVO #TODO embed javascript values poll.recipients.count=contactos seleccionados poll.messages.count=los mensajes se enviarán @@ -346,28 +431,27 @@ poll.response.enabled=Respuesta Automática Activada poll.message.edit=Editar el mensaje que se enviará a los destinatarios poll.message.prompt=El siguiente mensaje se enviará a los destinatarios de la encuesta poll.message.count=160 caracteres restantes (1 mensaje SMS) - poll.moreactions.delete=Eliminar encuesta poll.moreactions.rename=Renombrar encuesta poll.moreactions.edit=Editar encuesta poll.moreactions.export=Exportar encuesta - +folder.moreactions.delete=Eliminar carpeta +folder.moreactions.rename=Renombrar carpeta +folder.moreactions.export=Exportar carpeta #TODO embed javascript values -poll.reply.text=Responda "{0} {1}" para Sí, "{2} {3}" para No. -poll.reply.text1={0} "{1} {2}" para {3} +poll.reply.text=Responda "{0}" para Sí, "{1}" para No. +poll.reply.text1={0} "{1}" para {2} poll.reply.text2=Por favor responda 'Si' o 'No' -poll.reply.text3= o -poll.reply.text4={0} {1} +poll.reply.text3=o poll.reply.text5=Responda poll.reply.text6=Por favor responda poll.message.send={0} {1} poll.recipients.validation.error=Seleccione los contactos a quienes se enviará el mensaje -frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name = {1} {0} "{2}" debe ser único +frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name={1} {0} "{2}" debe ser único frontlinesms2.Poll.responses.validator.error.frontlinesms2.Poll.responses=Las opciones de respuesta no pueden ser idénticas -frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value = La palabra clave "{2}" ya ha sido utilizada. - +frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value=La palabra clave "{2}" ya ha sido utilizada wizard.title.new=Nuevo -wizard.fmessage.edit.title=Editar {0} +wizard.fmessage.edit.title=Editar {0} popup.title.saved={0} guardado! popup.activity.create=Crear Actividad Nueva : Seleccionar tipo popup.smartgroup.create=Crear grupo inteligente @@ -386,43 +470,43 @@ smallpopup.messages.export.title=Exportar Resultados ({0} mensajes) smallpopup.test.message.title=Mensaje de Prueba smallpopup.recipients.title=Destinatarios smallpopup.folder.title=Carpeta -smallpopup.group.title=Grupo smallpopup.contact.export.title=Exportar smallpopup.contact.delete.title=Eliminar contact.selected.many={0} contactos seleccionados group.join.reply.message=Bienvenido group.leave.reply.message=Adiós -fmessage.new.info=Tienes {0} mensajes nuevos. Haga click para ver -wizard.quickmessage.title=Mensaje Rápido +fmessage.new.info=Tiene {0} mensajes nuevos. Haga click para ver +wizard.quickmessage.title=Send Message wizard.messages.replyall.title=Responder a Todos wizard.send.message.title=Enviar mensaje -wizard.ok=Ok +wizard.ok=Aceptar wizard.create=Crear +wizard.save=Guardar wizard.send=Enviar common.settings=Ajustes common.help=Ayuda - +validation.nospaces.error=Palabras clave no deben tener espacios activity.validation.prompt=Por favor llene todos los campos requeridos +validator.invalid.name=Ya existe otra actividad con el nombre {2} autoreply.blank.keyword=Palabra clave en blanco. Se enviará una respuesta a todos los mensajes entrantes - - poll.type.prompt=Seleccione el tipo de encuesta a realizar -poll.question.yes.no=Pregunta con "Sí" o "No" como respuesta +poll.question.yes.no=Pregunta con con respuesta "Sí" o "No" poll.question.multiple=Pregunta de opción múltiple (e.g. 'Rojo', 'Azul', 'Verde') poll.question.prompt=Ingresar pregunta poll.message.none=No enviar un mensaje para esta encuesta (recolectar respuestas únicamente). -poll.replies.header=Responder automáticamente a las respuestas de la encuesta (opcional) +poll.replies.header=Responder automáticamente a las respuestas de la encuesta (opcional) poll.replies.description=Cuando se identifique un mensaje entrante como una respuesta a la encuesta, enviar un mensaje a la persona que envió la respuesta. poll.autoreply.send=Enviar una respuesta automática a las respuestas de la encuesta -poll.responses.prompt=Ingrese las posibles respuestas (entre 2 y 5) -poll.sort.header=Clasificar automáticamente los mensajes utilizando una palabra clave (opcional) +poll.responses.prompt=Ingrese las posibles respuestas (entre 2 y 5) +poll.sort.header=Clasificar automáticamente los mensajes utilizando una palabra clave (opcional) +poll.sort.enter.keywords=Ingrese las palabras clave de la encuesta y las respuestas poll.sort.description=Si la gente envía respuestas a la encuesta usando la palabra clave, FrontlineSMS puede clasificar automáticamente los mensajes en tu sistema. poll.no.automatic.sort=No clasificar los mensajes automáticamente poll.sort.automatically=Clasificar automáticamente los mensajes que contengan la siguiente palabra clave -poll.validation.prompt=Por favor llene todos los campos requeridos +poll.validation.prompt=Por favor complete todos los campos requeridos poll.name.validator.error.name=El nombre de la encuesta debe ser único pollResponse.value.blank.value=El valor de la respuesta a la encuesta no puede quedarse en blanco -poll.alias.validation.error.invalid.alias=Alias inválido. Intente con un, nombre, palabra +poll.keywords.validation.error.invalid.keyword=Palabra clave inválida. Pruebe letra, nombre, palabra (separados por comas) poll.question=Ingrese Pregunta poll.response=Lista de Respuestas poll.sort=Clasificación Automática @@ -431,6 +515,7 @@ poll.edit.message=Editar Mensaje poll.recipients=Seleccionar destinatarios poll.confirm=Confirmar poll.save=La encuesta ha sido guardada! +poll.save.success={0} Encuesta ha sido guardada! poll.messages.queue=Si eligió enviar un mensaje con esta encuesta, los mensajes se han agregado a la fila de mensajes pendientes. poll.messages.queue.status=El envío de todos los mensajes puede demorar dependiendo del número de mensajes y de la conexión de red. poll.pending.messages=Para ver el estado de su mensaje, abra el folder de mensajes 'Pendientes'. @@ -440,7 +525,7 @@ quickmessage.message.label=Mensaje quickmessage.message.none=Ninguno quickmessage.recipient.label=Destinatario quickmessage.recipients.label=Destinatarios -quickmessage.message.count=160 caracteres restantes (1 mensaje SMS) +quickmessage.message.count=160 caracteres restantes (1 mensaje SMS) quickmessage.enter.message=Ingresar un mensaje quickmessage.select.recipients=Seleccionar a los destinatarios quickmessage.confirm=Confirmar @@ -452,9 +537,8 @@ quickmessage.messages.label=Ingresar un mensaje quickmessage.phonenumber.label=Agregar un número telefónico: quickmessage.phonenumber.add=Agregar quickmessage.selected.recipients=destinatarios seleccionados -quickmessage.validation.prompt=Por favor llene todos los campos requeridos - -fmessage.number.error=Al guardar, los caracteres en este campo serán ignorados +quickmessage.validation.prompt=Por favor llene todos los campos obligatorios +fmessage.number.error=Non-numeric characters in this field will be removed when saved search.filter.label=Limitar la búsqueda a search.filter.group=Seleccionar grupo search.filter.activities=Seleccionar actividad/carpeta @@ -464,40 +548,39 @@ search.filter.sent=Sólo mensajes enviados search.filter.archive=Incluir archivo search.betweendates.label=Entre las fechas search.header=Buscar -search.quickmessage=Mensaje rápido +search.quickmessage=Send message search.export=Exportar los resultados -search.keyword.label=Palabra o frase clave +search.keyword.label=Palabra clave search.contact.name.label=Nombre del contacto search.contact.name=Nombre del contacto search.result.header=Resultados search.moreoptions.label=Más opciones settings.general=General +settings.porting=Importar y Exportar settings.connections=Teléfonos y conexiones settings.logs=Sistema settings.general.header=Ajustes > General -settings.logs.header=Registros del sistema +settings.logs.header=Settings > System Logs logs.none=No tiene ningún registro. logs.content=Mensaje logs.date=Tiempo logs.filter.label=Mostrar los registros para logs.filter.anytime=todo el tiempo -logs.filter.1day=últimas 24 horas -logs.filter.3days=últimos 3 días -logs.filter.7days=últimos 7 días -logs.filter.14days=últimos 14 días -logs.filter.28days=últimos 28 días +logs.filter.days.1=últimas 24 horas +logs.filter.days.3=últimos 3 días +logs.filter.days.7=últimos 7 días +logs.filter.days.14=últimos 14 días +logs.filter.days.28=últimos 28 días logs.download.label=Descargar los registros del sistema logs.download.buttontext=Descargar los registros logs.download.title=Descargar los registros para enviar logs.download.continue=Continuar - smartgroup.validation.prompt=Por favor llene todos los campos requeridos. Especifique únicamente una regla por campo. smartgroup.info=Para crear un grupo Inteligente, seleccione los criterios que deben cumplir los contactos para este grupo. smartgroup.contains.label=contiene smartgroup.startswith.label=comienza con smartgroup.add.anotherrule=Agregar otra regla smartgroup.name.label=Nombre - modem.port=Puerto modem.description=Descripción modem.locked=Bloqueado? @@ -511,57 +594,57 @@ traffic.all.folders.activities=Mostrar todas las actividades/carpetas traffic.sent=Enviados traffic.received=Recibidos traffic.total=Total - tab.message=Mensajes tab.archive=Archivo tab.contact=Contactos tab.status=Estado tab.search=Buscar - -help.info=Esta es una versión beta, de modo que no hay una sección de 'ayuda'. Por favor entre a los foros de usuarios para obtener ayuda por el momento. - +help.info=Esta es una versión beta, de modo que no hay una sección de 'ayuda'. Por favor entre a los foros de usuarios para obtener ayuda por el momento. +help.notfound=Este archivo de ayuda aún no está disponible, lo sentimos. # IntelliSms Fconnection -intellismsfconnection.label=Cuenta IntelliSms -intellismsfconnection.type.label=Tipo -intellismsfconnection.name.label=Nombre -intellismsfconnection.username.label=Usuario -intellismsfconnection.password.label=Contraseña - -intellismsfconnection.send.label=Utilizar para enviar -intellismsfconnection.receive.label=Utilizar para recibir -intellismsfconnection.receiveProtocol.label=Protocolo -intellismsfconnection.serverName.label=Nombre del servidor -intellismsfconnection.serverPort.label=Puerto del Servidor -intellismsfconnection.emailUserName.label=Usuario -intellismsfconnection.emailPassword.label=Contraseña - +intellisms.label=Cuenta IntelliSms +intellisms.type.label=Tipo +intellisms.name.label=Nombre +intellisms.username.label=Usuario +intellisms.password.label=Contraseña +intellisms.sendEnabled.label=Usar para enviar. +intellisms.receiveEnabled.label=Usar para recibir. +intellisms.receiveProtocol.label=Protocolo +intellisms.serverName.label=Nombre del servidor +intellisms.serverPort.label=Puerto del Servidor +intellisms.emailUserName.label=Usuario +intellisms.emailPassword.label=Contraseña +intellisms.description=Enviar y recibir mensajes a través de una cuenta Intellisms +intellisms.global.info=Usted tendrá que configurar una cuenta con Intellisms ( www.intellisms.co.uk ). +intelliSmsFconnection.send.validator.invalid=No se puede configurar una conexión sin las funciones de ENVIAR o RECIBIR +intelliSmsFconnection.receive.validator.invalid=No se puede configurar una conexión sin las funciones de ENVIAR o RECIBIR #Controllers contact.label=Contacto(s) -contact.edited.by.another.user=Otro contacto ha actualizado los datos de este Contacto mientras usted editaba +contact.edited.by.another.user=Otro usuario ha actualizado los datos de este Contacto mientras usted estaba editando contact.exists.prompt=Ya hay un contacto con ese número contact.exists.warn=Ya existe un contacto con este número contact.view.duplicate=Ver el contacto duplicado contact.addtogroup.error=No se puede agregar y eliminar del mismo grupo! -contact.mobile.label=Móbil o Celular -contact.email.label=Correo electrónico +contact.mobile.label=Móvil o Celular fconnection.label=Fconexión fconnection.name=Fconexión fconnection.unknown.type=Tipo de conexión desconocido: -fconnection.test.message.sent=Mensaje de prueba enviado! +fconnection.test.message.sent=Mensaje de prueba ha sido colocado en fila de envío! announcement.saved=Se ha guardado el anuncio y el(los) mensaje(s) se ha(n) colocado en la fila de envío announcement.not.saved=El anuncio no se pudo guardar! +announcement.save.success={0} El anuncio se ha guardado! announcement.id.exist.not=No se pudo encontrar el anuncio con id {0} -autoreply.saved=La respuesta automática ha sido guardada! -autoreply.not.saved=La respuesta automática no pudo guardarse! +autoreply.save.success={0} La respuesta automática se ha guardado! +autoreply.not.saved=La respuesta automática no se pudo guardar! report.creation.error=Error al crear el reporte export.message.title=Exportar Mensaje de FrontlineSMS export.database.id=ID de Base de Datos export.message.date.created=Fecha de creación export.message.text=Texto export.message.destination.name=Nombre de destino -export.message.destination.mobile=Móbil o Celular de destino +export.message.destination.mobile=Móvil o Celular de destino export.message.source.name=Nombre de origen -export.message.source.mobile=Móbil o Celular de origen +export.message.source.mobile=Móvil o Celular de origen export.contact.title=Exportar Contacto de FrontlineSMS export.contact.name=Nombre export.contact.mobile=Móbil o Celular @@ -573,108 +656,119 @@ export.messages.name2={0} (mensajes{1}) export.contacts.name1=Grupo {0} (contactos {1}) export.contacts.name2=Grupo Inteligente {0} (contactos {1}) export.contacts.name3=Todos los contactos (contactos {0}) -folder.label=Carpeta folder.archived.successfully=La carpeta se archivó exitosamente! folder.unarchived.successfully=La carpeta se extrajo exitosamente! folder.trashed=La carpeta fue enviada a la papelera! folder.restored=La carpeta se restauró exitosamente! folder.exist.not=No se pudo encontrar la carpeta con id {0} folder.renamed=Carpeta Renombrada - group.label=Grupo group.name.label=Nombre group.update.success=Grupo actualizado exitosamente -group.save.fail=Fallo al guardar el grupo +group.save.fail=Falla al guardar el grupo group.delete.fail=No se pudo eliminar el grupo - -import.label=Importar -import.backup.label=Importar datos de un respaldo previo -import.prompt.type=Seleccionar tipo de datos a importar -import.contacts=Información del contacto -import.messages=Detalles del mensaje -import.version1.info=Para importar datos desde la versión 1, favor de exportarlos en Inglés -import.prompt=Seleccione el archivo a importar +import.label=Import contacts and messages +import.backup.label=You can use the form below to import your contacts. You can also import messages that you have from a previous Frontline project. +import.prompt.type=Select the type of data you wish to import before uploading +import.messages=Messages in Frontline's CSV format +import.prompt=Select the file containing your data to initiate the import import.upload.failed=La carga del archivo falló por alguna razón. import.contact.save.error=Error al guardar el contacto import.contact.complete=Se importaron {0} contactos; {1} fallaron. -import.contact.failed.download=Descargar contactos fallidos (formato CSV) +import.contact.exist=Los contactos importados ya existen. +import.contact.failed.label=No se pudieron importar contactos +import.contact.failed.info={0} contact(s) successfully imported.
{1} contact(s) could not be imported.
{2} +import.download.failed.contacts=Descargar un archivo que contiene los contactos faillidos. import.message.save.error=Error al guardar el mensaje import.message.complete=Se importaron {0} mensajes; {1} fallaron. - -many.selected = {0} {1}s seleccionados - +export.label=Export data from your Frontline workspace +export.backup.label=You can export your Frontline data as VCF/VCard, CSV or PDF +export.prompt.type=Select which data you wish to export +export.allcontacts=All of your contacts +export.inboxmessages=Your Inbox messages +export.submit.label=Export and download data +many.selected={0} {1}s seleccionados flash.message.activity.found.not=No se encontró la actividad flash.message.folder.found.not=No se pudo encontrar la carpeta flash.message=Mensaje flash.message.fmessage={0} mensaje(s) -flash.message.fmessages.many={0} mensajes SMS -flash.message.fmessages.many.one=1 mensaje SMS +flash.message.fmessages.many={0} mensajes SMS +flash.message.fmessages.many.one=1 mensaje SMS fmessage.exist.not=No se pudo encontrar el mensaje id {0} -flash.message.poll.queued=Se ha guardado la encuesta y el mensaje(s) se agregó a la fila de envío. -flash.message.poll.saved=La encuesta se ha guardado +flash.message.poll.queued=Se ha guardado la encuesta y se agrego el(los) mensaje(s) a la fila de envío. flash.message.poll.not.saved=No se pudo guardar la encuesta! -system.notification.ok=OK +system.notification.ok=Aceptar system.notification.fail=FALLO flash.smartgroup.delete.unable=No fue posible eliminar el grupo inteligente flash.smartgroup.saved=Grupo Inteligente {0} guardado flash.smartgroup.save.failed=Error al guardar el grupo inteligente. Los errores fueron {0} smartgroup.id.exist.not=No se pudo encontrar el grupo inteligente con id {0} smartgroup.save.failed=Error al guardar el grupo inteligente{0} con parámetros {1}{2}errores: {3} -contact.name.label=Nombre -contact.phonenumber.label=Número telefónico - searchdescriptor.searching=Buscando -searchdescriptor.all.messages= todos los mensajes +searchdescriptor.all.messages=todos los mensajes searchdescriptor.archived.messages=, incluyendo mensajes archivados -searchdescriptor.exclude.archived.messages=, sin mensajes archivados +searchdescriptor.exclude.archived.messages=, sin los mensajes archivados searchdescriptor.only=, sólo {0} searchdescriptor.between=, entre {0} y {1} searchdescriptor.from=, desde {0} searchdescriptor.until=, hasta {0} -poll.title=encuesta {0} -announcement.title=anuncio {0} -autoreply.title=Respuesta Automática {0} -folder.title=Carpeta {0} +poll.title={0} +announcement.title={0} +autoreply.title={0} +folder.title={0} frontlinesms.welcome=Bienvenido a FrontlineSMS! \\o/ failed.pending.fmessages=Fallaron {0} mensaje(s) pendiente(s). Revíselos en la sección de mensajes pendientes. - +subscription.title={0} +subscription.info.group=Grupo: {0} +subscription.info.groupMemberCount={0} miembros +subscription.info.keyword=Palabras clave de nivel superior: {0} +subscription.sorting.disable=Desactivar ordenación automática +subscription.info.joinKeywords=Suscribir a: {0} +subscription.info.leaveKeywords=Dejar: {0} +subscription.group.goto=Ver Grupo +subscription.group.required.error=Las suscripciones deben tener un grupo +subscription.save.success={0} La suscripción se ha guardado! language.label=Idioma -language.prompt=Cambiar el idioma de la interfaz de usuario de FrontlineSMS +language.prompt=Cambiar el idioma de la interfaz de usuario de FrontlineSMS frontlinesms.user.support=Soporte a Usuarios de FrontlineSMS -download.logs.info1=ADVERTENCIA: El equipo de FrontlineSMS no puede atender directamente los problemas reportados. Si tiene una solicitud de soporte técnico, le recomendamos referirse a los archivos de Ayuda. Es probable que la respuesta a su problema esté ahí. De no encontrar respuesta, por favor reporte su problema en nuestros foros de ayuda para usuarios: -download.logs.info2=Es posible que otros usuarios hayan reportado el mismo problema y encontrado una solución! Para continuar con el envío de su reporte, haga clck en 'Continuar'. - +download.logs.info1=ADVERTENCIA: El equipo de FrontlineSMS no puede atender directamente los problemas reportados. Si tiene una solicitud de soporte técnico, le recomendamos referirse a los archivos de Ayuda. Es probable que la respuesta a su problema esté ahí. De no encontrar respuesta, por favor reporte su problema en nuestros foros de ayuda para usuarios: +download.logs.info2=Es posible que otros usuarios hayan reportado el mismo problema y encontrado una solución! Para continuar con el envío de su reporte, haga click en 'Continuar'. +# Configuration location info +configuration.location.title=Configuración de la ubicación +configuration.location.description=Estos archivos incluyen su base de datos y otras configuraciones, lo cuales sería recomendable que respalde en otra parte, +configuration.location.instructions=Usted puede encontrar la configuración de la aplicación en href="{0}">
. Estos archivos incluyen la base de datos y otros ajustes, lo que es posible que desee hacer copia de seguridad en otra parte. dynamicfield.contact_name.label=Nombre del Contacto dynamicfield.contact_number.label=Número del Contacto dynamicfield.keyword.label=Palabra Clave dynamicfield.message_content.label=Contenido del Mensaje - -# Fmessage domain +# TextMessage domain fmessage.queued=El mensaje se agregó a la fila para enviarse a {0} -fmessage.queued.multiple=El mensaje se agregó a la fila para enviarse a {0} destinatarios +fmessage.queued.multiple=El mensaje se agregó a la fila para enviarse a {0} destinatarios fmessage.retry.success=El mensaje se ha vuelto a agregar a la fila para enviarse a {0} fmessage.retry.success.multiple={0} mensaje(s) se han vuelto a agregar a la fila de envío fmessage.displayName.label=Nombre fmessage.text.label=Mensaje fmessage.date.label=Fecha fmessage.to=Para: {0} -fmessage.to.multiple=Para: {0} destinatarios -fmessage.quickmessage=Mensaje rápido +fmessage.to.multiple=Para: {0} destinatarios +fmessage.quickmessage=Send message fmessage.archive=Archivo fmessage.activity.archive=Archivar {0} fmessage.unarchive=Extraer {0} fmessage.export=Exportar fmessage.rename=Renombrar {0} -fmessage.edit=Editar {0} -fmessage.delete=Eliminar {0} +fmessage.edit=Editar {0} +fmessage.delete=Delete fmessage.moreactions=Más acciones... fmessage.footer.show=Mostrar -fmessage.footer.show.failed=Fallo +fmessage.footer.show.failed=Falló fmessage.footer.show.all=Todos fmessage.footer.show.starred=Favoritos +fmessage.footer.show.incoming=Entrante +fmessage.footer.show.outgoing=Saliente fmessage.archive.back=Atrás fmessage.activity.sentmessage=({0} mensajes enviados) -fmessage.failed=fallo +fmessage.failed=falló fmessage.header=mensajes fmessage.section.inbox=Bandeja de Entrada fmessage.section.sent=Enviados @@ -685,53 +779,47 @@ fmessage.resend=Reenviar fmessage.retry=Reintentar fmessage.reply=Responder fmessage.forward=Reenviar -fmessage.unarchive=Extraer -fmessage.delete=Eliminar fmessage.messages.none=Aquí no hay mensajes! fmessage.selected.none=No seleccionó ningún mensaje fmessage.move.to.header=Mover mensaje a... fmessage.move.to.inbox=Bandeja de Entrada -fmessage.archive.many=Archivar todos +fmessage.archive.many=Archive selected fmessage.count=1 mensaje fmessage.count.many={0} mensajes -fmessage.many= mensajes -fmessage.delete.many=Eliminar Todo -fmessage.reply.many=Responder a todos +fmessage.many=mensajes +fmessage.delete.many=Delete selected +fmessage.reply.many=Reply selected fmessage.restore=Restaurar fmessage.restore.many=Restaurar -fmessage.retry.many=Falló el reintento +fmessage.retry.many=Retry selected fmessage.selected.many={0} mensajes seleccionados -fmessage.unarchive.many=Extraer todo - +fmessage.unarchive.many=Unarchive selected # TODO move to poll.* fmessage.showpolldetails=Mostrar gráfico fmessage.hidepolldetails=Ocultar gráfico - # TODO move to search.* fmessage.search.none=No se encontraron mensajes fmessage.search.description=Iniciar nueva búsqueda del lado izquierdo - +fmessage.connection.receivedon=Recibido en: activity.name=Nombre activity.delete.prompt=Mover {0} a papelera. Esto colocará todos los mensajes asociados a él en la papelera. activity.label=Actividad activity.categorize=Categorizar respuesta - magicwand.title=Agregar expresiones sustitutas folder.create.success=Carpeta creada exitosamente folder.create.failed=No se pudo crear la carpeta folder.name.validator.error=Este nombre de carpeta ya ha sido utilizado -folder.name.blank.error=El nombre de la carpeta no puede quedarse en blanco -poll.name.blank.error=El nombre de la encuesta no puede quedarse en blanco +folder.name.blank.error=El nombre de la carpeta no puede quedar en blanco +poll.name.blank.error=El nombre de la encuesta no puede quedar en blanco poll.name.validator.error=Este nombre de encuesta ya ha sido utilizado -autoreply.name.blank.error=El nombre de la respuesta automática no puede quedarse en blanco +autoreply.name.blank.error=El nombre de la respuesta automática no puede quedar en blanco autoreply.name.validator.error=Este nombre de respuesta automática ya ha sido utilizado -announcement.name.blank.error=El nombre del anuncio no puede quedarse en blanco +announcement.name.blank.error=El nombre del anuncio no puede quedar en blanco announcement.name.validator.error=Este nombre de anuncio ya ha sido utilizado -group.name.blank.error=El nombre del grupo no puede quedarse en blanco +group.name.blank.error=El nombre del grupo no puede quedar en blanco group.name.validator.error=Este nombre del grupo ya ha sido utilizado - #Jquery Validation messages -jquery.validation.required=Este campo es requerido. +jquery.validation.required=Este campo es obligatorio. jquery.validation.remote=Por favor corrija este campo. jquery.validation.email=Por favor ingrese una dirección válida de correo electrónico. jquery.validation.url=Por favor ingrese un hipervínculo válido. @@ -748,4 +836,230 @@ jquery.validation.rangelength=Por favor ingrese un valor de entre {0} y {1} cara jquery.validation.range=Por favor ingrese un valor entre {0} y {1}. jquery.validation.max=Por favor ingrese un valor menor que o igual a {0}. jquery.validation.min=Por favor ingrese un valor mayor que o igual a {0}. - +# Webconnection common +webconnection.select.type=Seleccione Servicio Web o una aplicación para conectarse a la misma: +webconnection.type=Seleccione el tipo +webconnection.title={0} +webconnection.label=Conexión Web +webconnection.description=Conectar a servicio web. +webconnection.sorting=Ordenación automática +webconnection.configure=Configurar el servicio +webconnection.api=Exponer API +webconnection.api.info=FrontlineSMS puede estar configurado para recibir las solicitudes entrantes de su servicio remoto y desencadenar mensajes salientes. Para obtener más detalles, consulte la sección de ayuda Conexión Web +webconnection.api.enable.label=Habilitar API +webconnection.api.secret.label=API clave secreta: +webconnection.api.disabled=API disabled +webconnection.api.url=URL de la API +webconnection.moreactions.retryFailed=reintentar cargas fallidas +webconnection.failed.retried=Las conexiones fallidas a la web se han programado para un renvío. +webconnection.url.error.locahost.invalid.use.ip=Por favor use 127.0.0.1 en lugar de "localhost" para las urls del servidor local. +webconnection.url.error.url.start.with.http=URL inválida (debe iniciar con http:// or https://) +# Webconnection - generic +webconnection.generic.label=Otro servicio web +webconnection.generic.description=Enviar mensajes a otro servicio web +webconnection.generic.subtitle=HTTP Web Conexión +# Webconnection - Ushahidi/Crowdmap +webconnection.ushahidi.label=Crowdmap/Ushahidi +webconnection.ushahidi.description=Enviar mensajes a Crowdmap o a un servidor Ushahidi. +webconnection.ushahidi.key.description=La clave de la API para Crowdmap o Ushahidi se puede encontrar en la configuración de la Crowdmap o en el sitio web Ushahidi. +webconnection.ushahidi.url.label=Ushahidi deployment address: +webconnection.ushahidi.key.label=Clave API Ushahidi: +webconnection.crowdmap.url.label=Dirección de implementación Crowdmap: +webconnection.crowdmap.key.label=Clave API Crowdmap: +webconnection.ushahidi.serviceType.label=Seleccione Servicio +webconnection.ushahidi.serviceType.crowdmap=Crowdmap +webconnection.ushahidi.serviceType.ushahidi=Ushahidi +webconnection.crowdmap.url.suffix.label=.crowdmap.com +webconnection.ushahidi.subtitle=Conexión Web a: {0} +webconnection.ushahidi.service.label=Servicio: +webconnection.ushahidi.fsmskey.label=Secreto API FrontlineSMS: +webconnection.ushahidi.crowdmapkey.label=Clave API Crowdmap/Ushahidi: +webconnection.ushahidi.keyword.label=Palabra clave: +url.invalid.url=La URL proprcionada es inválida +webconnection.confirm=Confirmar +webconnection.keyword.title=Transfiera cada mensaje recibido que contiene la siguiente palabra clave: +webconnection.all.messages=No utilice la palabra clave (Todos los mensajes entrantes se reenviarán a esta conexión web) +webconnection.httpMethod.label=Seleccione Método HTTP: +webconnection.httpMethod.get=GET +webconnection.httpMethod.post=POST +webconnection.name.prompt=Nombre esta conexión web +webconnection.details.label=Confirmar detalles +webconnection.parameters=Configurar información enviada al servidor +webconnection.parameters.confirm=Información configurada enviada al servidor +webconnection.keyword.label=Palabra clave: +webconnection.none.label=Ninguno +webconnection.url.label=Url del servidor: +webconnection.param.name=Nombre: +webconnection.param.value=Valor: +webconnection.add.anotherparam=Añadir parámetro +dynamicfield.message_body.label=Texto del mensaje +dynamicfield.message_body_with_keyword.label=Texto del mensaje con palabra clave +dynamicfield.message_src_number.label=Número de contacto +dynamicfield.message_src_name.label=Nombre de contacto +dynamicfield.message_timestamp.label=Fecha y hora del mensaje +webconnection.keyword.validation.error=Se requiere palabra clave +webconnection.url.validation.error=Se requiere Url +webconnection.save=La conexión web se ha guardado! +webconnection.saved=Conexión web guardada! +webconnection.save.success={0} La conexión web se ha guardado! +webconnection.generic.service.label=Servicio: +webconnection.generic.httpMethod.label=Método Http: +webconnection.generic.url.label=Dirección: +webconnection.generic.parameters.label=Información configurada enviada al servidor: +webconnection.generic.keyword.label=Palabra clave: +webconnection.generic.key.label=Clave API: +frontlinesms2.Keyword.value.validator.error.frontlinesms2.UshahidiWebconnection.keyword.value=Valor inválido para la palabra clave +#Subscription i18n +subscription.label=Suscripción +subscription.name.prompt=Nombre de la suscripción +subscription.details.label=Confirme los detalles +subscription.description=Permite a la gente unirse de forma automática y también dejar los grupos de contacto utilizando un mensaje con palabra clave +subscription.select.group=Seleccione el grupo para la suscripción +subscription.group.none.selected=Seleccione el grupo +subscription.autoreplies=Respuesta automática +subscription.sorting=Ordenación automática +subscription.sorting.header=Procesar los mensajes automáticamente utilizando una palabra clave (opcional) +subscription.confirm=Confirmar +subscription.group.header=Seleccione el grupo +subscription.group.description=Los contactos se pueden agregar y quitar de los grupos automáticamente cuando FrontlineSMS recibe un mensaje que incluye una palabra clave especial. +subscription.keyword.header=Ingrese palabras clave para esta suscripción +subscription.top.keyword.description=Ingrese las palabras clave de alto nivel que los usuarios utilizan para seleccionar este grupo. +subscription.top.keyword.more.description=Puede escribir varias palabras clave de alto nivel para cada opción, separados por comas. Palabras clave de alto nivel deben ser únicos en todas las actividades. +subscription.keywords.header=Ingrese palabras clave para unirse y dejar este grupo. +subscription.keywords.description=Puede escribir varias palabras clave para cada opción, separadas por comas. Si no hay palabras clave de alto nivel, las palabras clave para unirse y dejar deben ser únicas en todas las actividades. +subscription.default.action.header=Seleccione una acción cuando no se envió ninguna palabra clave +subscription.default.action.description=Seleccione la acción deseada cuando un mensaje coincida con la palabra clave de alto nivel, pero no con las palabras clave para unirse o dejar: +subscription.keywords.leave=Dejar palabra(s) clave +subscription.keywords.join=Suscribir palabra(s) clave +subscription.default.action.join=Suscribir el contacto al grupo +subscription.default.action.leave=Eliminar el contacto del grupo +subscription.default.action.toggle=Alternar suscripción del contacto con el grupo +subscription.autoreply.join=Enviar una respuesta automática cuando un contacto se suscribe al grupo +subscription.autoreply.leave=Enviar una respuesta automática cuando un contacto deja el grupo +subscription.confirm.group=Grupo +subscription.confirm.keyword=Palabra clave +subscription.confirm.join.alias=Suscribir palabra clave +subscription.confirm.leave.alias=Dejar palabra clave +subscription.confirm.default.action=Acción por defecto +subscription.confirm.join.autoreply=Suscribir respuesta automática +subscription.confirm.leave.autoreply=Dejar respuesta automática +subscription.info1=La suscripción se ha guardado y ya está activa +subscription.info2=Los mensajes entrantes que coincidan con esta palabra clave cambiarán la membresía del grupo de contactos tal como se haya definido +subscription.info3=Para ver la suscripción, haga clic en el menú de la izquierda. +subscription.categorise.title=Categorizar los mensajes +subscription.categorise.info=Por favor, seleccione la acción que se realizará con los remitentes de los mensajes seleccionados cuando se añadan a {0} +subscription.categorise.join.label=Suscribir remitentes a {0} +subscription.categorise.leave.label=Retire los remitentes de {0} +subscription.categorise.toggle.label=Alternar remitentes' miembros de {0} +subscription.join=Suscribir +subscription.leave=Dejar +subscription.sorting.example.toplevel=por ejemplo SOLUCIÓN +subscription.sorting.example.join=por ejemplo SUSCRÍBETE, ÚNETE +subscription.sorting.example.leave=por ejemplo DEJAR, DETENER +subscription.keyword.required=Se requier palabra clave +subscription.jointext.required=Por favor ingrese respuesta automática a la solicitud de suscripción +subscription.leavetext.required=Por favor ingrese respuesta automática a la solicitud de dejar +subscription.moreactions.delete=Eliminar suscripción +subscription.moreactions.rename=Cambiar el nombre de la suscripción +subscription.moreactions.edit=Editar suscripción +subscription.moreactions.export=Exportar suscripción +# Generic activity sorting +activity.generic.sorting=Proceso automático +activity.generic.sorting.subtitle=Procesar los mensajes automáticamente utilizando una palabra clave (opcional) +activity.generic.sort.header=Procesar los mensajes automáticamente utilizando una palabra clave (opcional) +activity.generic.sort.description=Si la gente envíen mensajes que comienzan con una determinada palabra clave, FrontlineSMS automáticamente puede procesar los mensajes en su sistema. +activity.generic.keywords.title=Ingrese palabras clave para la actividad. Puede ingresar varias palabras clave separadas por comas: +activity.generic.keywords.subtitle=Ingrese palabras clave para la actividad. +activity.generic.keywords.info=Puede ingresar varias palabras clave separadas por comas: +activity.generic.no.keywords.title=No utilice una palabra clave +activity.generic.no.keywords.description=Todos los mensajes entrantes que no coincidan con las otras palabras clave activarán esta actividad +activity.generic.disable.sorting=No ordenar mensajes automáticamente +activity.generic.disable.sorting.description=Los mensajes no serán procesados automáticamente por esta actividad +activity.generic.enable.sorting=Procesar automáticamente las respuestas que contengan la palabra clave +activity.generic.sort.validation.unique.error=Las palabras clave deben ser únicas +activity.generic.keyword.in.use=La palabra clave {0} ya está en uso por la actividad {1} +activity.generic.global.keyword.in.use=Actividad {0} está configurado para recibir todos los mensajes que no coincidan con ninguna palabra clave. Sólo puede tener una actividad activo con este ajuste +#basic authentication +auth.basic.label=Autenticación básica +auth.basic.info=Nombre de usuario y contraseña para acceder a FrontlineSMS en la red +auth.basic.enabled.label=Habilitar Autenticación Básica +auth.basic.username.label=Nombre de usuario +auth.basic.password.label=Contraseña +auth.basic.confirmPassword.label=Confirmar Contraseña +auth.basic.password.mismatch=Las contraseñas no coinciden +newfeatures.popup.title=Nuevas funciones +newfeatures.popup.showinfuture=Mostrar esta de diálogo en el futuro +dynamicfield.message_text.label=Texto del mensaje +dynamicfield.message_text_with_keyword.label=Texto del mensaje con palabra clave +dynamicfield.sender_name.label=Nombre del remitente +dynamicfield.sender_number.label=Número del remitente +dynamicfield.recipient_number.label=Número del destinatario +dynamicfield.recipient_name.label=Nombre del destinatario +# Smpp Fconnection +smpp.label=Cuenta SMPP +smpp.type.label=Tipo +smpp.name.label=Nombre +smpp.send.label=Usar para enviar +smpp.receive.label=Usar para recibir +smpp.url.label=URL de la SMCS +smpp.port.label=Puerto SMSC +smpp.username.label=Nombre de usuario +smpp.password.label=Contraseña +smpp.fromNumber.label=Número de +smpp.description=Enviar y recibir mensaje por medio de un SMSC +smpp.global.info=Necesitará obtener una cuenta de su red telefónica de su elección. +smpp.send.validator.invalid=No puede configurar una conexión sin la funcionalidad ENVIAR o RECIBIR. +routing.title=Cree reglas para decidir qué número de teléfono de usara para mensajes salientes. +routing.info=Estas reglas determinarán como el sisteme elije qué conexión o número de teléfono usar para enviar mensajes salientes. Recuerde, el número de teléfono visto por los destinatarios puede depender de las reglas que usted estableció aquí. Además, cambiar esta configuración puede afectar el costo de envío de los mensajes. +routing.rules.sending=Al enviar los mensajes salientes: +routing.rules.not_selected=Si una de las reglas anteriores coinciden: +routing.rules.otherwise=De otra forma; +routing.rules.device=Use {0} +routing.rule.uselastreceiver=Enviar por medio del número más reciente que al que el contacto envió un mensaje +routing.rule.useany=Usar cualquier número de teléfono disponible +routing.rule.dontsend=No enviar el mensaje +routing.notification.no-available-route=El(los) mensaje(s) saliente(s) no se enviaron debido a sus preferencias de enrutamiento. +routing.rules.none-selected.warning=Aviso: Usted no tiene reglas o números de teléfono seleccionados. No se enviaron mensajes. Si desea enviar mensajes, por favor habilite una conexión. +customactivity.overview=Vision general +customactivity.title={0} +customactivity.confirm=Confirme +customactivity.label=Constructor de Actividad Personalizada +customactivity.description=Cree su propia actividad desde cero usando aplicando un conjunto personalizado de acciones para su palabra especificada. +customactivity.name.prompt=Nombre este actividad +customactivity.moreactions.delete=Eliminar actividad +customactivity.moreactions.rename=Renombrar actividad +customactivity.moreactions.edit=Editar actividad +customactivity.moreactions.export=Exportar actividad +customactivity.text.none=Ninguno +customactivity.config=Configurar +customactivity.config.description=Contriuya y configure un conjunto de acciones para resta actividad. Las acciones se ejecutarán cuando un mensaje coincide con el criterio que estableció en el paso anterior. +customactivity.info=Se ha creado su Actividad Personalizada, y se aplicará las acciones especificadas en cualquier mensaje que contenga su palabra clave. +customactivity.info.warning=Sin una palabra clave, todos los mensajes entrantes activarán las acciones de esta Actividad Personalizada. +customactivity.info.note=Nota: Si usted archiva la Actividad Personalizada, los mensajes entrantes ya no se ordenarán para la Actividad Personalizada. +customactivity.save.success={0} actividad guardada +customactivity.action.steps.label=Pasos de acción +validation.group.notnull=Por favor seleccione un grupo +customactivity.join.description=Unirse al grupo "{0}" +customactivity.leave.description=Abandonar el grupo "{0}" +customactivity.forward.description=Renviar con "{0}" +customactivity.webconnectionStep.description=Subiendo a "{0}" +customactivity.reply.description=Responder con "{0}" +customactivity.step.join.add=Agregar remitente al grupo +customactivity.step.join.title=Agregar remitente al grupo* +customactivity.step.leave.add=Retirar los remitentes del grupo +customactivity.step.leave.title=Remover del grupo al remitente* +customactivity.step.reply.add=Enviar Autorespuesta +customactivity.step.reply.title=Introducir mensaje de autorespuesta al remitente* +customactivity.step.forward.add=Reenviar mensaje +customactivity.step.forward.title=Reenviar automáticamente un mensaje a uno o más contactos +customactivity.manual.sorting=Procesamiento automático desactivado +customactivity.step.webconnectionStep.add=Cargar mensaje a una URL +customactivity.step.webconnectionStep.title=Cargar mensaje a una URL +customactivity.validation.error.autoreplytext=Se requiere un mensaje de respuesta +customactivity.validation.error.name=Se requiere el nombre +customactivity.validation.error.url=Se requiere Url +customactivity.validation.error.paramname=Se requiere el nombre del parámetro +recipientSelector.keepTyping=Seguir intentando... +recipientSelector.searching=Buscando... +validation.recipients.notnull=Por favor seleccione al menos a un destinatario. +localhost.ip.placeholder=su dirección ip diff --git a/plugins/frontlinesms-core/grails-app/i18n/messages_fr.properties b/plugins/frontlinesms-core/grails-app/i18n/messages_fr.properties index dae1391e1..c4df011e6 100644 --- a/plugins/frontlinesms-core/grails-app/i18n/messages_fr.properties +++ b/plugins/frontlinesms-core/grails-app/i18n/messages_fr.properties @@ -1,751 +1,697 @@ -# FrontlineSMS English translation by the FrontlineSMS team, Nairobi -language.name=French - -# General info -app.version.label=Version - -# Common action imperatives - to be used for button labels and similar -action.ok=Ok -action.close=Fermer -action.cancel=Annuler -action.done=Compléter -action.next=Suivant -action.prev=Précédent -action.back=Retour -action.create=créer -action.edit=Modifier -action.rename=Renommer -action.save=Enregistrer -action.save.all=Tout enregistrer -action.delete=Supprimer -action.delete.all=Tout supprimer -action.send=Envoyer -action.export=Exporter - -# Messages when FrontlineSMS server connection is lost -server.connection.fail.title=La connexion au serveur a été perdue. -server.connection.fail.info=S'il vous plaît redémarrez FrontlineSMS ou fermez cette fenêtre. - -#Connections: -connection.creation.failed=La connexion n'a pas pu être créée {0} -connection.route.destroyed=Itinéraire détruit de {0} à {1} -connection.route.connecting=Connexion en cours... -connection.route.disconnecting=Déconnexion en cours... -connection.route.successNotification=Itinéraire créé avec succès sur {0} -connection.route.failNotification=Impossible de créer un itinéraire {1}: {2} [modifier] -connection.route.destroyNotification=Itinéraire déconnecté sur {0} -connection.test.sent=Message test envoyé avec succès à {0} en utilisant {1} -# Connection exception messages -connection.error.org.smslib.alreadyconnectedexception=Dispositif déjà connecté -connection.error.org.smslib.gsmnetworkregistrationexception=Enregistrement impossible sur le réseau GSM -connection.error.org.smslib.invalidpinexception=PIN incorrect -connection.error.org.smslib.nopinexception=Le PIN nécessaire mais non fourni -connection.error.java.io.ioexception=Erreur du port{0} - -connection.header=Connexions -connection.list.none=Aucune connexion configurée -connection.edit=Modifier la connexion -connection.delete=Supprimer la connexion -connection.deleted=La connexion {0} a été supprimée -connection.route.create=Créer un itinéraire -connection.add=Ajouter une nouvelle connexion -connection.createtest.message.label=Message -connection.route.destroy=Détruire la route -connection.send.test.message=Envoyer un message test -connection.test.message=Félicitations de la part FrontlineSMS \\o/ vous avez correctement configuré {0} pour envoyer des SMS \\o/ -connection.validation.prompt=S'il vous plaît remplir tous les champs obligatoires -connection.select=Sélectionnez le type de connexion -connection.type=Choisissez le type de -connection.details=Entrez les détails -connection.confirm=Confirmez -connection.createtest.number=Numéro -connection.confirm.header=Confirmer les paramètres -connection.name.autoconfigured=Auto-configuré {0} {1} sur le port {2}" - -status.connection.header=Connexions -status.connection.none=Vous n'avez configuré aucune connexion. -status.devises.header=Dispositifs détectés -status.detect.modems=Modems détectés -status.modems.none=Aucun dispositif n'a été détecté pour le moment. - -connectionstatus.not_connected=Non connecté -connectionstatus.connecting=Connexion en cours -connectionstatus.connected=Connecté - -default.doesnt.match.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] ne correspond pas au modèle requis [{3}] -default.invalid.url.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] n'est pas une URL valide -default.invalid.creditCard.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] n'est pas un numéro de carte bancaire valide -default.invalid.email.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] n'est pas une adresse e-mail valide -default.invalid.range.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] n'entre pas de la plage valide allant de [{3}] à [{4}] -default.invalid.size.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] n'entre pas dans la gamme de taille allant de [{3}] à [{4}] -default.invalid.max.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] dépasse la valeur maximale [{3}] -default.invalid.min.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] est inférieure à la valeur minimale [{3}] -default.invalid.max.size.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] est supérieure à la taille maximale des [{3}] -default.invalid.min.size.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] est inférieure à la taille minimum de [{3}] -default.invalid.validator.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] ne passe pas la validation personnalisée -default.not.inlist.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] n'est pas contenue dans la liste [{3}] -default.blank.message=La propriété [{0}] de classe [{1}] ne peut pas être vide -default.not.equal.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] peut ne pas correspondre à [{3}] -default.null.message=La propriété [{0}] de classe [{1}] ne peut pas être nulle -default.not.unique.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] doit être unique - -default.paginate.prev=Précédent -default.paginate.next=Suivant -default.boolean.true=Vrai -default.boolean.false=Faux -default.date.format=jj mm aaaa hh:mm -default.number.format=0 - -default.unarchived={0} désarchivé -default.unarchive.failed=Désarchivage {0} a échoué -default.trashed={0} effacé -default.restored={0} restauré -default.restore.failed=Impossible de restaurer {0} avec id {1} -default.archived={0} archivé avec succès! -default.archived.multiple={0} archivé -default.created={0} créé -default.created.message={0} {1} a été créé -default.create.failed=Impossible de créer {0} -default.updated={0} a été mis à jour -default.update.failed=Impossible de mettre à jour {0} avec id {1} -default.updated.multiple= {0} ont été mis à jour -default.updated.message={0} mis à jour -default.deleted={0} supprimé -default.trashed={0} placés dans la corbeille -default.trashed.multiple={0} placés dans la corbeille -default.archived={0} archivé -default.unarchived={0} desarchivé -default.unarchive.keyword.failed=Désarchivage {0} a échoué. Mot-clé est déjà en cours d'utilisation -default.unarchived.multiple={0} désarchivé -default.delete.failed=Impossible de supprimer {0} avec id {1} -default.notfound=Impossible de trouver {0} avec id {1} -default.optimistic.locking.failure=Un autre utilisateur a mis à jour ce {0} alors que vous étiez en train de modifier - -default.home.label=Acuueil -default.list.label={0} Liste -default.add.label=Ajouter {0} -default.new.label=Nouveau {0} -default.create.label=Créer {0} -default.show.label=Afficher {0} -default.edit.label=Modifier {0} -search.clear=Effacer la recherche - -default.button.create.label=Créer -default.button.edit.label=Modifier -default.button.update.label=Mettre à jour -default.button.delete.label=Supprimer -default.button.search.label=Rechercher -default.button.apply.label=Appliquer -default.button.delete.confirm.message=Êtes-vous sûr? - -default.deleted.message={0} supprimé - -# Data binding errors. Use "typeMismatch.$className.$La propriétéName to customize (eg typeMismatch.Book.author) -typeMismatch.java.net.URL=La propriété {0} doit être une URL valide -typeMismatch.java.net.URI=La propriété {0} doit être un URI valide -typeMismatch.java.util.Date=La propriété {0} doit être une date valide -typeMismatch.java.lang.Double=La propriété {0} doit être un numéro valide -typeMismatch.java.lang.Integer=La propriété {0} doit être un numéro valide -typeMismatch.java.lang.Long=La propriété {0} doit être un numéro valide -typeMismatch.java.lang.Short=La propriété {0} doit être un numéro valide -typeMismatch.java.math.BigDecimal=La propriété {0} doit être un numéro valide -typeMismatch.java.math.BigInteger=La propriété {0} doit être un numéro valide -typeMismatch.int = {0} doit être un numéro valide - -# Application specific messages -messages.trash.confirmation=Ceci videra la corbeille et supprimera les messages de façon permanente. Voulez-vous continuer? -default.created.poll=Le sondage a été créé! -default.search.label=Effacer la recherche -default.search.betweendates.title=Entre les dates: -default.search.moresearchoption.label=Plus d'options de recherche -default.search.date.format=j/m/aaaa -default.search.moreoption.label=Plus d'options - -# SMSLib Fconnection -smslibfconnection.label=Téléphone / Modem -smslibfconnection.type.label=Type -smslibfconnection.name.label=Nom -smslibfconnection.port.label=Port -smslibfconnection.baud.label=Vitesse de transmission -smslibfconnection.pin.label=PIN -smslibfconnection.imsi.label=SIM IMSI -smslibfconnection.serial.label=Numéro de série # -smslibfconnection.send.label=Utilisez Modem pour l'envoi de messages SEULEMENT -smslibfconnection.receive.label=Utilisez Modem pour recevoir des messages SEULEMENT -smslibFconnection.send.validator.error.send=Modem doit être utilisé pour envoyer -smslibFconnection.receive.validator.error.receive=ou recevoir des messages - -# Email Fconnection -emailfconnection.label=Email -emailfconnection.type.label=Type -emailfconnection.name.label=Nom -emailfconnection.receiveProtocol.label=Protocole -emailfconnection.serverName.label=Nom du serveur -emailfconnection.serverPort.label=Port du serveur -emailfconnection.username.label=Nom d'utilisateur -emailfconnection.password.label=Mot de passe - -# CLickatell Fconnection -clickatellfconnection.label=Compte Clickatell -clickatellfconnection.type.label=Type -clickatellfconnection.name.label=Nom -clickatellfconnection.apiId.label=ID de l'API -clickatellfconnection.username.label=Nom d'utilisateur -clickatellfconnection.password.label=Mot de passe - -# Messages Tab -message.create.prompt=Entrez le message -message.character.count=Caractères restants {0} ({1} SMS (s)) -message.character.count.warning=Peut être plus lent après l'exécution de substitutions -announcement.label=Annonce -announcement.description=Envoyer un message d'annonce et organiser les réponses -announcement.info1=L'annonce a été enregistrée et les messages ont été ajoutés à la file de messages en attente. -announcement.info2=Il peut prendre un certain temps pour que tous les messages soient envoyés, tout dépendant du nombre de messages et de la connexion réseau. -announcement.info3=Pour voir l'état de vos messages, ouvrez le dossier des messages «en attente» . -announcement.info4=Pour voir l'annonce cliquez dessus dans le menu à gauche. -announcement.validation.prompt=S'il vous plaît remplir tous les champs obligatoires -announcement.select.recipients=Sélectionner les destinataires -announcement.confirm=Confirmer -announcement.delete.warn=Supprimer {0} AVERTISSEMENT: Impossible d'annuler cette action! -announcement.prompt=Nommez cette annonce -announcement.confirm.message=Message -announcement.details.label=Confirmez les détails -announcement.message.label=Message -announcement.message.none=aucun -announcement.recipients.label=Les destinataires -announcement.create.message=Créer un message -#TODO embed javascript values -announcement.recipients.count=contacts sélectionnés -announcement.messages.count=les messages seront envoyés -announcement.moreactions.delete=Supprimer l'annonce -announcement.moreactions.rename=Renommer l'annonce -announcement.moreactions.edit=Modifier l'annonce -announcement.moreactions.export=Exporter l'annonce -frontlinesms2.Announcement.name.unique.error.frontlinesms2.Announcement.name={1} {0} "{2}" doivent être uniques - - -archive.inbox=Boîte de réception des archives -archive.sent=Archives envoyées -archive.activity=Archive des activités -archive.folder=Dossier d'archives -archive.folder.name=Nom -archive.folder.date=Date -archive.folder.messages=Messages -archive.folder.none=  Pas de dossiers archivés -archive.activity.name=Nom -archive.activity.type=Type -archive.activity.date=Date -archive.activity.messages=Messages -archive.activity.list.none=  Aucune activité archivée -Autoreply.enter.keyword=Entrez un mot-clé -Autoreply.create.message=Entrez le message -Autoreply.confirm=Confirmer -Autoreply.name.label=Message -Autoreply.details.label=Confirmer les details -Autoreply.label=Reponse Automatique -Autoreply.keyword.label=Mot-clé -Autoreply.description=Répondre automatiquement aux messages entrants -Autoreply.info=Le message de réponse automatique a été créé, tous les messages contenant votre mot-clé sera ajoutée à cette activité de message de réponse automatique, qui peut être consultée en cliquant dessus dans le menu de droite. -Autoreply.info.warning=Les réponses automatiques sans mot-clé seront envoyées à tous les messages entrants -Autoreply.info.note=Note: Si vous archivez le message de réponse automatique, les messages entrants ne seront plus triés. -Autoreply.validation.prompt=S'il-vous-plaît remplir tous les champs obligatoires -Autoreply.message.title=Message à être renvoyé pour cette réponse automatique: -Autoreply.keyword.title=Trier les messages automatiquement en utilisant un mot clé: -Autoreply.name.prompt=Nommez cette réponse automatique -Autoreply.message.count=0 characters (1 SMS message) -Autoreply.moreactions.delete=Supprimer la réponse automatique -Autoreply.moreactions.rename=Renommer la réponse automatique -Autoreply.moreactions.edit=Modifier la réponse automatique -Autoreply.moreactions.export=Exporter la réponse automatique -Autoreply.all.messages=Ne pas utiliser le mot clé (Tous les messages entrants recevront ce message automatique) -Autoreply.text.none=Aucun -frontlinesms2.au.name.unique.error.frontlinesms2.Autoreply.name={1} {0} "{2}" doivent être uniques -frontlinesms2.Autoreply.name.validator.error.frontlinesms2.Autoreply.name=Le nom de la réponse automatique doit être unique -frontlinesms2.Keyword.value.validator.error.frontlinesms2.Autoreply.keyword.value=Le mot-clé"{2}" est déjà utilisé -frontlinesms2.Autoreply.AutoreplyText.nullable.error.frontlinesms2.Autoreply.AutoreplyText=Le message ne peut pas être vide - -contact.new=Nouveau contact -contact.list.no.contact=Aucun contact ici! -contact.header=Contacts -contact.all.contacts=Tous les contacts -contact.create=Créer un nouveau contact -contact.groups.header=Groupes -contact.create.group=Créer un nouveau groupe -contact.smartgroup.header=Groupes intelligents -contact.create.smartgroup=Créer un nouveau groupe intelligent -contact.add.to.group=Ajouter au groupe... -contact.remove.from.group=Retirer du groupe -contact.customfield.addmoreinformation=Ajouter plus d'informations ... -contact.customfield.option.createnew=Créer un nouveau ... -contact.name.label=Nom -contact.phonenumber.label=Mobile -contact.phonenumber.international.warning=Ce nombre n'est pas au format international. Cela peut causer des problèmes pour faire correspondre lees messages aux contacts. -contact.notes.label=Notes -contact.email.label=Email -contact.groups.label=Groupes -contact.notinanygroup.label=Ne fait pas partie d'un groupe -contact.messages.label=Messages -contact.sent.messages={0} messages envoyés -contact.received.messages={0} messages reçus -contact.search.messages=Rechercher parmi les messages - -group.rename=Renommer le groupe -group.edit=Modifier le groupe -group.delete=Supprimer le groupe -group.moreactions=Plus actions ... - -customfield.validation.prompt=S'il vous plaît inscrire un nom -customfield.name.label=Nom -export.contact.info=Pour exporter des contacts à partir de FrontlineSMS, choisissez le type d'exportation et l'information à inclure dans les données exportées. -export.message.info=Pour exporter des messages de FrontlineSMS, choisissez le type d'exportation et l'information à inclure dans les données exportées. -export.selectformat=Sélectionnez un format de sortie -export.csv=Format CSV pour une utilisation dans le tableau -export.pdf=Format PDF pour l'impression -folder.name.label=Nom -group.delete.prompt=Etes-vous sûr de vouloir supprimer {0}? AVERTISSEMENT: Ceci ne peut être annulé. -layout.settings.header=Paramètres -activities.header=Activités -activities.create=Créer une nouvelle activité -folder.header=Dossiers -folder.create=Créer un nouveau dossier -folder.label=dossier -message.folder.header={0} Dossier -fmessage.trash.actions=Actions de la corbeille ... -fmessage.trash.empty=Vider la corbeille -fmessage.to.label=à -trash.empty.prompt=Tous les messages et les activités dans la poubelle seront supprimés définitivement -fmessage.responses.total={0} Total des réponses -fmessage.label=Message -fmessage.label.multiple={0} messages -poll.prompt=Nommez ce sondage -poll.details.label=Confirmez les détails -poll.message.label=Message -poll.choice.validation.error.deleting.response=Un choix enregistré ne peut pas avoir une valeur vide -poll.alias=Alias -poll.aliases.prompt=Entrez les alias pour les options correspondantes. -poll.aliases.prompt.details=Vous pouvez entrer plusieurs alias pour chaque option, séparés par des virgules. Le premier alias sera envoyé dans le message d'intrsuctions du sondage. -poll.alias.validation.error=Les alias doivent être uniques -poll.sort.label=Tri automatique -poll.autosort.no.description=Messages ne seront pas triées automatiquement. -poll.autosort.description=Trier les messages avec le mot-clé -poll.sort.keyword=mot-clé -poll.sort.by=Trier par -poll.Autoreply.label=Réponse Automatique -poll.Autoreply.none=aucun -poll.recipients.label=Destinataires -poll.recipients.none=Aucun -#TODO embed javascript values -poll.recipients.count=contacts sélectionnés -poll.messages.count=les messages seront envoyés -poll.yes=Oui -poll.no=Non -poll.label=Sondage -poll.description=Envoyer une question et analyser les réponses -poll.messages.sent={0} messages envoyés -poll.response.enabled=Réponse automatique activée -poll.message.edit=Modifier le message à envoyer aux destinataires -poll.message.prompt=Le message suivant sera envoyé aux destinataires du sondage -poll.message.count=Caractères restants 160 (1 SMS) - -poll.moreactions.delete=Supprimer le sondage -poll.moreactions.rename=Renommer le sondage -poll.moreactions.edit=Modifier le sondage -poll.moreactions.export=Exporter sondage - -#TODO embed javascript values -poll.reply.text=Répondre"{0} {1}" pour Yes, "{2} {3}" pour No. -poll.reply.text1={0} "{1} {2}" pour {3} -poll.reply.text2=S'il vous plaît répondez 'Oui' ou 'Non' -poll.reply.text3= ou -poll.reply.text4={0} {1} -poll.reply.text5=Répondre -poll.reply.text6=S'il vous plaît répondre à -poll.message.send={0} {1} -poll.recipients.validation.error=Sélectionnez les contacts à qui envoyer les messages -frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name = {1} {0} "{2}" doivent être uniques -frontlinesms2.Poll.responses.validator.error.frontlinesms2.Poll.responses=Les options de réponse ne peuvent pas être identiques -frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value = Mot clé "{2}" est déjà en cours d'utilisation - -wizard.title.new=Nouveau -wizard.fmessage.edit.title=Modifier {0} -popup.title.saved={0} enregistré! -popup.activity.create=Créer une nouvelle activité: Sélectionnez le type de -popup.smartgroup.create=Créer un groupe intelligent -popup.help.title=Aide -smallpopup.customfield.create.title=Créer des champs personnalisés -smallpopup.group.rename.title=Renommer le groupe -smallpopup.group.edit.title=Modifier le groupe -smallpopup.group.delete.title=Supprimer le groupe -smallpopup.fmessage.rename.title=Renommer {0} -smallpopup.fmessage.delete.title=Supprimer {0} -smallpopup.fmessage.export.title=Exporter -smallpopup.delete.prompt=Supprimer {0} ? -smallpopup.delete.many.prompt=Supprimer {0} contacts? -smallpopup.empty.trash.prompt=Vider la corbeille? -smallpopup.messages.export.title=Exporter les Results ({0} messages) -smallpopup.test.message.title=Message de Test -smallpopup.recipients.title=Destinataires -smallpopup.folder.title=Dossier -smallpopup.group.title=Groupe -smallpopup.contact.export.title=Exporter -smallpopup.contact.delete.title=Supprimer -contact.selected.many={0} contacts selectionnés -group.join.reply.message=Bienvenue -group.leave.reply.message=Au revoir -fmessage.new.info=Vous avez {0} nouveaux messages. Cliquez pour voir -wizard.quickmessage.title=Message rapide -wizard.messages.replyall.title=Répondre à tous -wizard.send.message.title=Envoyer message -wizard.ok=Ok -wizard.create=Créer -wizard.send=Envoyer -common.settings=Paramètres -common.help=Aide - -activity.validation.prompt=S'il-vous-plaît remplir tous les champs obligatoires -Autoreply.blank.keyword=Mot-clé vide. Une réponse sera envoyée à tous les messages entrants - - -poll.type.prompt=Sélectionnez le type de sondage à créer -poll.question.yes.no=Question à réponse «Oui» ou «Non» -poll.question.multiple=Question à choix multiples (e.g. 'Rouge', 'Blue', 'Vert') -poll.question.prompt=Entrez question -poll.message.none=Ne pas envoyer de message pour ce sondage (recueillir les réponses seulement). -poll.replies.header=Répondre automatiquement aux entrées du sondage (facultatif) -poll.replies.description=Lorsque un message entrant est identifié comme une réponse sondage, envoyez un message à la personne qui a envoyé la réponse. -poll.Autoreply.send=Envoyer une réponse automatique aux réponses du sondage -poll.responses.prompt=Entrer des réponses possibles, (entre 2 et 5) -poll.sort.header=Trier les messages automatiquement en utilisant un mot-clé (facultatif) -poll.sort.description=Si les gens envoient des réponses aux sondages en utilisant un mot-clé, FrontlineSMS peut trier automatiquement les messages sur votre système. -poll.no.automatic.sort=Ne pas trier automatiquement les messages -poll.sort.automatically=Trier automatiquement les messages qui ont le mot-clé suivant -poll.validation.prompt=S'il-vous-plaît remplir tous les champs obligatoires -poll.name.validator.error.name=Les noms du sondage doivent être uniques -pollResponse.value.blank.value=La valeur de la réponse du sondage ne peut pas être vide -poll.alias.validation.error.invalid.alias=Alias non valides. Essayez un, nom, un mot -poll.question=Entrez question -poll.response=Liste de réponse -poll.sort=Tri automatique -poll.reply=Réponse automatique -poll.edit.message=Modifier le message -poll.recipients=Sélectionner les destinataires -poll.confirm=Confirmer -poll.save=Le sondage a été enregistré -poll.messages.queue=Si vous avez choisi d'envoyer des messages avec ce sondage, les messages ont été ajoutés à la file de messages en attente. -poll.messages.queue.status=Il peut prendre un certain temps pour que tous les messages soient envoyés en fonction du nombre de messages et de la connexion réseau. -poll.pending.messages=Pour voir l'état de votre message, ouvrez le dossier 'en attente' des messages. -poll.send.messages.none=Aucun message ne sera envoyé -quickmessage.details.label=Confirmez les détails -quickmessage.message.label=Message -quickmessage.message.none=Aucun -quickmessage.recipient.label=Destinataire -quickmessage.recipients.label=Destinataires -quickmessage.message.count=Caractères restants 160 (1 SMS) -quickmessage.enter.message=Entrer message -quickmessage.select.recipients=Selectionnez les destinataires -quickmessage.confirm=Confirmer -#TODO embed javascript values -quickmessage.recipients.count=contacts sélectionnés -quickmessage.messages.count=les messages seront envoyés -quickmessage.count.label=Nombre de messages: -quickmessage.messages.label=Entrer le message -quickmessage.phonenumber.label=Ajouter le numéro de téléphone: -quickmessage.phonenumber.add=Ajouter -quickmessage.selected.recipients=destinataires sélectionnés -quickmessage.validation.prompt=S'il vous plaît remplir tous les champs obligatoires - -fmessage.number.error=Les caractères dans ce champ seront supprimés lors de l'enregistrement -search.filter.label=Limiter la recherche à -search.filter.group=Sélectionnez le groupe -search.filter.activities=Sélectionnez l'activité / dossier -search.filter.messages.all=Tous les messages envoyés et reçus -search.filter.inbox=Seulement les messages reçus -search.filter.sent=Seulement les messages envoyés -search.filter.archive=Inclure archives -search.betweendates.label=Entre les dates -search.header=Rechercher -search.quickmessage=Message rapide -search.export=Exporter les résultats -search.keyword.label=Mot-clé ou une phrase -search.contact.name.label=Nom du contact -search.contact.name=Nom du contact -search.result.header=Résultats -search.moreoptions.label=Plus d'options -settings.general=Général -settings.connections=Téléphones et connexions -settings.logs=Système -settings.general.header=Paramètres> Général -settings.logs.header=Journaux de bord du système -logs.none=Vous n'avez pas les journaux de bord. -logs.content=Message -logs.date=Temps -logs.filter.label=Afficher les journaux debord pour -logs.filter.anytime=tous -logs.filter.1day=dernières 24 heures -logs.filter.3days=les trois derniers jours -logs.filter.7days=les sept derniers jours -logs.filter.14days=les 14 derniers jours -logs.filter.28days=les 28 derniers jours -logs.download.label=Télécharger les journaux de bord du système -logs.download.buttontext=Télécharger les journaux -logs.download.title=Télécharger les journaux à envoyer -logs.download.continue=Continuer - -smartgroup.validation.prompt=S'il vous plaît remplir tous les champs obligatoires. Vous ne pouvez spécifier qu'une règle par champ. -smartgroup.info=Pour créer un groupe intelligent, sélectionnez les critères adaptés dont vous avez besoin pour les contacts de ce groupe -smartgroup.contains.label=contient -smartgroup.startswith.label=commence avec -smartgroup.add.anotherrule=Ajouter une autre règle -smartgroup.name.label=Nom - -modem.port=Port -modem.description=Description -modem.locked=Verrouillé? -traffic.header=Trafic -traffic.update.chart=Mise à jour graphique -traffic.filter.2weeks=Afficher les dernières deux semaines -traffic.filter.between.dates=entre les dates -traffic.filter.reset=Réinitialiser les filtres -traffic.allgroups=Afficher tous les groupes -traffic.all.folders.activities=Afficher toutes les activités et les dossiers -traffic.sent=Envoyé -traffic.received=Reçu -traffic.total=Total - -tab.message=Messages -tab.archive=Archive -tab.contact=Contacts -tab.status=Statut -tab.search=Rechercher - -help.info=Ceci est une version une beta, il n'y a pas donc pas d'aide intégrée. S'il vous plaît allez sur les forums d'utilisateurs pour obtenir de l'aide à ce stade - -# IntelliSms Fconnection -intellismsfconnection.label=Compte IntelliSms -intellismsfconnection.type.label=Type -intellismsfconnection.name.label=Nom -intellismsfconnection.username.label=Nom d'utilisateur -intellismsfconnection.password.label=Mot de passe - -intellismsfconnection.send.label=Utiliser pour l'envoi -intellismsfconnection.receive.label=Utiliser pour recevoir -intellismsfconnection.receiveProtocol.label=Protocole -intellismsfconnection.serverName.label=Nom du serveur -intellismsfconnection.serverPort.label=Port du serveur -intellismsfconnection.emailUserName.label=Nom d'utilisateur -intellismsfconnection.emailPassword.label=Mot de Passe - -#Controllers -contact.label=Contact(s) -contact.edited.by.another.user=Un autre utilisateur a mis à jour ce contact alors que vous étiez en train de modifier -contact.exists.prompt=Il existe déjà un contact avec ce numéro -contact.exists.warn=Le contact avec ce numéro existe déjà -contact.view.duplicate=Voir dupliquer -contact.addtogroup.error=Impossible d'ajouter et de supprimer dans le même groupe! -contact.mobile.label=Mobile -contact.email.label=Email -fconnection.label=Fconnection -fconnection.name=Fconnection -fconnection.unknown.type=Type de connexion inconnu: -fconnection.test.message.sent=Message de test envoyé! -announcement.saved=L'annonce a été enregistrée et le message ou les messages ont été mis en attente pour être envoyés -announcement.not.saved=L'annonce n'a pas pu être enregistrée! -announcement.id.exist.not=Impossible de trouver l'annonce avec l'ID {0} -Autoreply.saved=La réponse automatique a été enregistrée ! -Autoreply.not.saved=La réponse automatique ne peut pas être enregistrée ! -report.creation.error=Erreur de création de rapport -export.message.title=Exporter le message FrontlineSMS -export.database.id=DatabaseID -export.message.date.created=Date de création -export.message.text=Texte -export.message.destination.name=Nom de la destination -export.message.destination.mobile=Mobile de la destination -export.message.source.name=Nom de la source -export.message.source.mobile=Mobile de la source -export.contact.title=Exporter les contacts FrontlineSMS -export.contact.name=Nom -export.contact.mobile=Mobile -export.contact.email=Email -export.contact.notes=Notes -export.contact.groups=Groupes -export.messages.name1={0} {1} ({2} messages) -export.messages.name2={0} ({1} messages) -export.contacts.name1={0} groupe ({1} contacts) -export.contacts.name2={0} group intelligent ({1} contacts) -export.contacts.name3=Tous les contacts ({0} contacts) -folder.label=Dossier -folder.archived.successfully=Le dossier a été archivé avec succès! -folder.unarchived.successfully=Le dossier a été désarchivé avec succès! -folder.trashed=Le dossier a été mis à la corbeille! -folder.restored=Le dossier a été restauré! -folder.exist.not=Impossible de trouver le dossier avec l'ID{0} -folder.renamed=Dossier renommé - -group.label=Groupe -group.name.label=Nom -group.update.success=Groupe mis à jour avec succès -group.save.fail=L'enregistrement du groupe a échoué -group.delete.fail=Impossible de supprimer le groupe - -import.label=Importer -import.backup.label=Importer les données provenant d'une sauvegarde précédente -import.prompt.type=Sélectionnez le type de données à importer -import.contacts=Coordonnées du contacts -import.messages=Détails du message -import.version1.info=Pour importer des données de la version 1, s'il vous plaît exportez les en anglais -import.prompt=Sélectionnez un fichier de données à importer -import.upload.failed=L'envoi du fichier a échoué pour une raison quelconque. -import.contact.save.error=Une erreur a été rencontrée lors de l'enregistrement du contact -import.contact.complete={0} les contacts ont été importés; {1} échoué -import.contact.failed.download=Télécharger les contacts qui ont échoué (CSV) -import.message.save.error=Une erreur a été rencontrée lors de l'enregistrement du message -import.message.complete={0} les messages ont été importés; {1} échoué - -many.selected = {0} {1}s sélectionné - -flash.message.activity.found.not=Cette activité n'a pu être trouvée -flash.message.folder.found.not=Le dossier n'a pas pu être trouvé -flash.message=Message -flash.message.fmessage={0} message(s) -flash.message.fmessages.many={0} messages SMS -flash.message.fmessages.many.one=1 message SMS -fmessage.exist.not=Impossible de trouver un message avec ID {0} -flash.message.poll.queued=Sondage a été enregistré et le message ou les messages(s) ont été mis en attente pour être envoyé(s) -flash.message.poll.saved=Le sondage a été enregistré -flash.message.poll.not.saved=Le sondage ne peut pas être enregistré -system.notification.ok=Ok -system.notification.fail=ECHEC -flash.smartgroup.delete.unable=Impossible de supprimer le groupe intelligent -flash.smartgroup.saved=Groupe Intelligent {0} enregistré -flash.smartgroup.save.failed=L'enregistrement du groupe intelligent a échoué. Les erreurs étaient{0} -smartgroup.id.exist.not=Impossible de trouver groupe intelligent avec id {0} -smartgroup.save.failed=Impossible d'enregistrer le groupe intelligent {0} avec les paramètres {1}-{2} erreurs: {3} -contact.name.label=Nom -contact.phonenumber.label=Numéro de téléphone - -searchdescriptor.searching=Recherche en cours -searchdescriptor.all.messages= tous les messages -searchdescriptor.archived.messages=, y compris les messages archivés -searchdescriptor.exclude.archived.messages=, sans les messages archivés -searchdescriptor.only=, seulement {0} -searchdescriptor.between=, entre {0} et {1} -searchdescriptor.from=, à partir de {0} -searchdescriptor.until=, jusqu'à {0} -poll.title={0} sondage -announcement.title={0} annonce -Autoreply.title={0} Réponse automatique -folder.title={0} dossier -frontlinesms.welcome=Bienvenue à FrontlineSMS! \\o/ -failed.pending.fmessages={0} message en attente (s) a échoué. Allez à la section des messages en attente pour voir. - -language.label=langue -language.prompt=Changer la langue de l'interface utilisateur FrontlineSMS -frontlinesms.user.support=Soutien aux utilisateurs FrontlineSMS -download.logs.info1=AVERTISSEMENT: L'équipe de FrontlineSMS est incapable de répondre directement aux journaux de bord soumis. Si vous avez fait une demande de support utilisateur, s'il vous plaît vérifiez les fichiers d'aide pour voir si vous pouvez y trouver la réponse. Si ce n'est pas le cas, signalez votre problème via nos forums de soutien aux utilisateurs: -download.logs.info2=Les autres utilisateurs peuvent déjà avoir signalé le même problème et trouvé une solution! Pour continuer et soumettre vos journaux, s'il vous plaît cliquez sur «Continuer» - -dynamicfield.contact_name.label=Nom du contact -dynamicfield.contact_number.label=Numero du contact -dynamicfield.keyword.label=Mot-Clé -dynamicfield.message_content.label=Contenu du message - -# Fmessage domain -fmessage.queued=Le message a été mis en attente pour être envoyer à {0} -fmessage.queued.multiple=Le message a été mis en attente pour être envoyer à {0} recipients -fmessage.retry.success=Le message a été remis en attente pour être envoyer à {0} -fmessage.retry.success.multiple={0} message(s) a été remis en attente pour être envoyer à -fmessage.displayName.label=Nom -fmessage.text.label=Message -fmessage.date.label=Date -fmessage.to=A: {0} -fmessage.to.multiple=A: {0} destinataires -fmessage.quickmessage=Message rapide -fmessage.archive=Archive -fmessage.activity.archive=Archive {0} -fmessage.unarchive=Non-archive {0} -fmessage.export=Exporter -fmessage.rename=Renommer {0} -fmessage.edit=Modifier {0} -fmessage.delete=Supprimer {0} -fmessage.moreactions=Plus actions ... -fmessage.footer.show=Afficher -fmessage.footer.show.failed=Echoué -fmessage.footer.show.all=Tous -fmessage.footer.show.starred=Favoris -fmessage.archive.back=Retour -fmessage.activity.sentmessage=({0} messages envoyés) -fmessage.failed=échoué -fmessage.header=messages -fmessage.section.inbox=Boîte de réception -fmessage.section.sent=Envoyés -fmessage.section.pending=En attente -fmessage.section.trash=Corbeille -fmessage.addsender=Ajouter aux contacts -fmessage.resend=Renvoyer -fmessage.retry=Réessayer -fmessage.reply=Répondre -fmessage.forward=Transférer -fmessage.unarchive=Désarchiver -fmessage.delete=Supprimer -fmessage.messages.none=Aucun messages ici! -fmessage.selected.none=Aucun message sélectionné -fmessage.move.to.header=Déplacer le message vers ... -fmessage.move.to.inbox=Boîte de réception -fmessage.archive.many=Tout archiver -fmessage.count=1 message -fmessage.count.many={0} messages -fmessage.many= messages -fmessage.delete.many=Tout supprimer -fmessage.reply.many=Répondre à tous -fmessage.restore=Restaurer -fmessage.restore.many=Restaurer -fmessage.retry.many=La nouvelle tentative a échoué -fmessage.selected.many={0} messages selectionnés -fmessage.unarchive.many=Désarchiver tout - -# TODO move to poll.* -fmessage.showpolldetails=Afficher le graphique -fmessage.hidepolldetails=Cacher le graphique - -# TODO move to search.* -fmessage.search.none=Aucun message trouvé -fmessage.search.description=Démarrer une nouvelle recherche sur la gauche - -activity.name=Nom -activity.delete.prompt=Déplacez {0} à la corbeille. Cela consistera à transférer tous les messages associés à la section poubelle. -activity.label=Activité -activity.categorize=Catégoriser réponse. - -magicwand.title=Ajoutez des expressions de substitution -folder.create.success=Dossier créé avec succès -folder.create.failed=Impossible de créer le dossier -folder.name.validator.error=Le nom du dossier utilisé -folder.name.blank.error=Le nom du dossier ne peut pas être vide -poll.name.blank.error=Le nom du Sondage ne peut pas être vide -poll.name.validator.error=Nom de sondage utilisé -Autoreply.name.blank.error=Le nom de la réponse automatique ne peut pas être vide -Autoreply.name.validator.error=nom de la réponse automatique utilisé -announcement.name.blank.error=Le nom de l'annonce ne peut pas être vide -announcement.name.validator.error=Nom de l'annonce utilisé -group.name.blank.error=Le nom du groupe ne peut pas être vide -group.name.validator.error=Nom du groupe utilisé - -#Jquery Validation messages -jquery.validation.required=Ce champ est obligatoire. -jquery.validation.remote=S'il vous plaît corrigez ce champ -jquery.validation.email=S'il vous plaît entrez une adresse email valide. -jquery.validation.url=S'il vous plaît entrez une URL valide. -jquery.validation.date=S'il vous plaît entrez une date valide. -jquery.validation.dateISO=S'il vous plaît entrez une date valide (ISO). -jquery.validation.number=S'il vous plaît entrez un numero valide. -jquery.validation.digits=S'il vous plaît entrez que des chiffres. -jquery.validation.creditcard=S'il vous plaît entrez un numéro de carte de crédit valide. -jquery.validation.equalto=S'il vous plaît entrez la même valeur à nouveau. -jquery.validation.accept=S'il vous plaît entrez une valeur avec une extension valide. -jquery.validation.maxlength=S'il vous plaît ne pas entrer plus de {0} caractères. -jquery.validation.minlength=S'il vous plaît entrez au moins {0} caractères. -jquery.validation.rangelength=S'il vous plaît entrez une valeur comprise entre {0} et {1} caractères. -jquery.validation.range=S'il vous plaît entrez une valeur comprise entre {0} et {1}. -jquery.validation.max=S'il vous plaît entrez une valeur inférieure ou égale à {0}. -jquery.validation.min=S'il vous plaît entrez une valeur supérieure ou égale à {0}. - +# FrontlineSMS English translation by the FrontlineSMS team, Nairobi +language.name=French +# General info +app.version.label=Version#### +# Common action imperatives - to be used for button labels and similar +action.ok=Ok +action.close=Fermer +action.cancel=Annuler +action.done=Compléter +action.next=Suivant +action.prev=Précédent +action.back=Retour +action.create=créer +action.edit=Modifier +action.rename=Renommer +action.save=Enregistrer +action.save.all=Tout enregistrer +action.delete=Supprimer +action.delete.all=Tout supprimer +action.send=Envoyer +action.export=Exporter +# Messages when FrontlineSMS server connection is lost +server.connection.fail.title=La connexion au serveur a été perdue. +server.connection.fail.info=S'il vous plaît redémarrez FrontlineSMS ou fermez cette fenêtre. +#Connections: +connection.creation.failed=La connexion n'a pas pu être créée {0} +connection.route.disabled=Itinéraire détruit de {0} à {1} +connection.route.connecting=Connexion en cours... +connection.route.disconnecting=Déconnexion en cours... +connection.route.successNotification=Itinéraire créé avec succès sur {0} +connection.route.failNotification=Impossible de créer un itinéraire {1}: {2} [[[modifier]](({0}))] +connection.route.disableNotification=Itinéraire déconnecté sur {0} +connection.test.sent=Message test envoyé avec succès à {0} en utilisant {1} +# Connection exception messages +connection.error.org.smslib.alreadyconnectedexception=Dispositif déjà connecté +connection.error.org.smslib.gsmnetworkregistrationexception=Enregistrement impossible sur le réseau GSM +connection.error.org.smslib.invalidpinexception=PIN incorrect +connection.error.org.smslib.nopinexception=Le PIN nécessaire mais non fourni +connection.error.java.io.ioexception=Erreur du port{0} +connection.header=Connexions +connection.list.none=Aucune connexion configurée +connection.edit=Modifier la connexion +connection.delete=Supprimer la connexion +connection.deleted=La connexion {0} a été supprimée +connection.route.create=Créer un itinéraire +connection.add=Ajouter une nouvelle connexion +connection.createtest.message.label=Message +connection.route.disable=Détruire la route +connection.send.test.message=Envoyer un message test +connection.test.message=Félicitations de la part FrontlineSMS \\o/ vous avez correctement configuré {0} pour envoyer des SMS \\o/ +connection.validation.prompt=S'il vous plaît remplir tous les champs obligatoires +connection.select=Sélectionnez le type de connexion +connection.type=Choisissez le type de +connection.details=Entrez les détails +connection.confirm=Confirmez +connection.createtest.number=Numéro +connection.confirm.header=Confirmer les paramètres +connection.name.autoconfigured=Auto-configuré {0} {1} sur le port {2}" +status.connection.header=Connexions +status.connection.none=Vous n'avez configuré aucune connexion. +status.devises.header=Dispositifs détectés +status.detect.modems=Modems détectés +status.modems.none=Aucun dispositif n'a été détecté pour le moment. +connectionstatus.not_connected=Non connecté +connectionstatus.connecting=Connexion en cours +connectionstatus.connected=Connecté +default.doesnt.match.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] ne correspond pas au modèle requis [{3}] +default.invalid.url.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] n'est pas une URL valide +default.invalid.creditCard.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] n'est pas un numéro de carte bancaire valide +default.invalid.email.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] n'est pas une adresse e-mail valide +default.invalid.range.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] n'entre pas de la plage valide allant de [{3}] à [{4}] +default.invalid.size.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] n'entre pas dans la gamme de taille allant de [{3}] à [{4}] +default.invalid.max.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] dépasse la valeur maximale [{3}] +default.invalid.min.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] est inférieure à la valeur minimale [{3}] +default.invalid.max.size.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] est supérieure à la taille maximale des [{3}] +default.invalid.min.size.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] est inférieure à la taille minimum de [{3}] +default.invalid.validator.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] ne passe pas la validation personnalisée +default.not.inlist.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] n'est pas contenue dans la liste [{3}] +default.blank.message=La propriété [{0}] de classe [{1}] ne peut pas être vide +default.not.equal.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] peut ne pas correspondre à [{3}] +default.null.message=La propriété [{0}] de classe [{1}] ne peut pas être nulle +default.not.unique.message=La propriété [{0}] de classe [{1}] avec la valeur [{2}] doit être unique +default.paginate.prev=Précédent +default.paginate.next=Suivant +default.boolean.true=Vrai +default.boolean.false=Faux +default.number.format=0 +default.unarchived={0} désarchivé +default.unarchive.failed=Désarchivage {0} a échoué +default.trashed={0} effacé +default.restored={0} restauré +default.restore.failed=Impossible de restaurer {0} avec id {1} +default.archived={0} archivé avec succès! +default.archived.multiple={0} archivé +default.created={0} créé +default.created.message={0} {1} a été créé +default.create.failed=Impossible de créer {0} +default.updated={0} a été mis à jour +default.update.failed=Impossible de mettre à jour {0} avec id {1} +default.updated.multiple={0} ont été mis à jour +default.updated.message={0} mis à jour +default.deleted={0} supprimé +default.trashed={0} placés dans la corbeille +default.trashed.multiple={0} placés dans la corbeille +default.archived={0} archivé +default.unarchived={0} desarchivé +default.unarchive.keyword.failed=Désarchivage {0} a échoué. Mot-clé est déjà en cours d'utilisation +default.unarchived.multiple={0} désarchivé +default.delete.failed=Impossible de supprimer {0} avec id {1} +default.notfound=Impossible de trouver {0} avec id {1} +default.optimistic.locking.failure=Un autre utilisateur a mis à jour ce {0} alors que vous étiez en train de modifier +default.home.label=Acuueil +default.list.label={0} Liste +default.add.label=Ajouter {0} +default.new.label=Nouveau {0} +default.create.label=Créer {0} +default.show.label=Afficher {0} +default.edit.label=Modifier {0} +search.clear=Effacer la recherche +default.button.create.label=Créer +default.button.edit.label=Modifier +default.button.update.label=Mettre à jour +default.button.delete.label=Supprimer +default.button.search.label=Rechercher +default.button.apply.label=Appliquer +default.button.delete.confirm.message=Êtes-vous sûr? +default.deleted.message={0} supprimé +# Data binding errors. Use "typeMismatch.$className.$La propriétéName to customize (eg typeMismatch.Book.author) +typeMismatch.java.net.URL=La propriété {0} doit être une URL valide +typeMismatch.java.net.URI=La propriété {0} doit être un URI valide +typeMismatch.java.util.Date=La propriété {0} doit être une date valide +typeMismatch.java.lang.Double=La propriété {0} doit être un numéro valide +typeMismatch.java.lang.Integer=La propriété {0} doit être un numéro valide +typeMismatch.java.lang.Long=La propriété {0} doit être un numéro valide +typeMismatch.java.lang.Short=La propriété {0} doit être un numéro valide +typeMismatch.java.math.BigDecimal=La propriété {0} doit être un numéro valide +typeMismatch.java.math.BigInteger=La propriété {0} doit être un numéro valide +typeMismatch.int={0} doit être un numéro valide +# Application specific messages +messages.trash.confirmation=Ceci videra la corbeille et supprimera les messages de façon permanente. Voulez-vous continuer? +default.created.poll=Le sondage a été créé! +default.search.label=Effacer la recherche +default.search.betweendates.title=Entre les dates: +default.search.moresearchoption.label=Plus d'options de recherche +default.search.moreoption.label=Plus d'options +# SMSLib Fconnection +smslibfconnection.label=Téléphone / Modem +smslibfconnection.type.label=Type +smslibfconnection.name.label=Nom +smslibfconnection.port.label=Port +smslibfconnection.baud.label=Vitesse de transmission +smslibfconnection.pin.label=PIN +smslibfconnection.imsi.label=SIM IMSI +smslibfconnection.serial.label=Numéro de série # +smslibfconnection.send.label=Utilisez Modem pour l'envoi de messages SEULEMENT +smslibfconnection.receive.label=Utilisez Modem pour recevoir des messages SEULEMENT +smslibFconnection.send.validator.error.send=Modem doit être utilisé pour envoyer +smslibFconnection.receive.validator.error.receive=ou recevoir des messages +# Email Fconnection +emailfconnection.label=Email +emailfconnection.type.label=Type +emailfconnection.name.label=Nom +emailfconnection.receiveProtocol.label=Protocole +emailfconnection.serverName.label=Nom du serveur +emailfconnection.serverPort.label=Port du serveur +emailfconnection.username.label=Nom d'utilisateur +emailfconnection.password.label=Mot de passe +# CLickatell Fconnection +clickatellfconnection.label=Compte Clickatell +clickatellfconnection.type.label=Type +clickatellfconnection.name.label=Nom +clickatellfconnection.apiId.label=ID de l'API +clickatellfconnection.username.label=Nom d'utilisateur +clickatellfconnection.password.label=Mot de passe +# Messages Tab +message.create.prompt=Entrez le message +message.character.count=Caractères restants {0} ({1} SMS (s)) +message.character.count.warning=Peut être plus lent après l'exécution de substitutions +announcement.label=Annonce +announcement.description=Envoyer un message d'annonce et organiser les réponses +announcement.info1=L'annonce a été enregistrée et les messages ont été ajoutés à la file de messages en attente. +announcement.info2=Il peut prendre un certain temps pour que tous les messages soient envoyés, tout dépendant du nombre de messages et de la connexion réseau. +announcement.info3=Pour voir l'état de vos messages, ouvrez le dossier des messages «en attente» . +announcement.info4=Pour voir l'annonce cliquez dessus dans le menu à gauche. +announcement.validation.prompt=S'il vous plaît remplir tous les champs obligatoires +announcement.select.recipients=Sélectionner les destinataires +announcement.confirm=Confirmer +announcement.delete.warn=Supprimer {0} AVERTISSEMENT: Impossible d'annuler cette action! +announcement.prompt=Nommez cette annonce +announcement.confirm.message=Message +announcement.details.label=Confirmez les détails +announcement.message.label=Message +announcement.message.none=aucun +announcement.recipients.label=Les destinataires +announcement.create.message=Créer un message +#TODO embed javascript values +announcement.recipients.count=contacts sélectionnés +announcement.messages.count=les messages seront envoyés +announcement.moreactions.delete=Supprimer l'annonce +announcement.moreactions.rename=Renommer l'annonce +announcement.moreactions.edit=Modifier l'annonce +announcement.moreactions.export=Exporter l'annonce +frontlinesms2.Announcement.name.unique.error.frontlinesms2.Announcement.name={1} {0} "{2}" doivent être uniques +archive.inbox=Boîte de réception des archives +archive.sent=Archives envoyées +archive.activity=Archive des activités +archive.folder=Dossier d'archives +archive.folder.name=Nom +archive.folder.date=Date +archive.folder.messages=Messages +archive.folder.none=  Pas de dossiers archivés +archive.activity.name=Nom +archive.activity.type=Type +archive.activity.date=Date +archive.activity.messages=Messages +archive.activity.list.none=  Aucune activité archivée +Autoreply.enter.keyword=Entrez un mot-clé +Autoreply.create.message=Entrez le message +Autoreply.confirm=Confirmer +Autoreply.name.label=Message +Autoreply.details.label=Confirmer les details +Autoreply.label=Reponse Automatique +Autoreply.keyword.label=Mot-clé +Autoreply.description=Répondre automatiquement aux messages entrants +Autoreply.info=Le message de réponse automatique a été créé, tous les messages contenant votre mot-clé sera ajoutée à cette activité de message de réponse automatique, qui peut être consultée en cliquant dessus dans le menu de droite. +Autoreply.info.warning=Les réponses automatiques sans mot-clé seront envoyées à tous les messages entrants +Autoreply.info.note=Note: Si vous archivez le message de réponse automatique, les messages entrants ne seront plus triés. +Autoreply.validation.prompt=S'il-vous-plaît remplir tous les champs obligatoires +Autoreply.message.title=Message à être renvoyé pour cette réponse automatique: +Autoreply.keyword.title=Trier les messages automatiquement en utilisant un mot clé: +Autoreply.name.prompt=Nommez cette réponse automatique +Autoreply.message.count=0 characters (1 SMS message) +Autoreply.moreactions.delete=Supprimer la réponse automatique +Autoreply.moreactions.rename=Renommer la réponse automatique +Autoreply.moreactions.edit=Modifier la réponse automatique +Autoreply.moreactions.export=Exporter la réponse automatique +Autoreply.all.messages=Ne pas utiliser le mot clé (Tous les messages entrants recevront ce message automatique) +Autoreply.text.none=Aucun +frontlinesms2.au.name.unique.error.frontlinesms2.Autoreply.name={1} {0} "{2}" doivent être uniques +frontlinesms2.Autoreply.name.validator.error.frontlinesms2.Autoreply.name=Le nom de la réponse automatique doit être unique +frontlinesms2.Keyword.value.validator.error.frontlinesms2.Autoreply.keyword.value=Le mot-clé"{2}" est déjà utilisé +frontlinesms2.Autoreply.AutoreplyText.nullable.error.frontlinesms2.Autoreply.AutoreplyText=Le message ne peut pas être vide +contact.new=Nouveau contact +contact.list.no.contact=Aucun contact ici! +contact.header=Contacts +contact.all.contacts=Tous les contacts +contact.create=Créer un nouveau contact +contact.groups.header=Groupes +contact.create.group=Créer un nouveau groupe +contact.smartgroup.header=Groupes intelligents +contact.create.smartgroup=Créer un nouveau groupe intelligent +contact.add.to.group=Ajouter au groupe... +contact.remove.from.group=Retirer du groupe +contact.customfield.addmoreinformation=Ajouter plus d'informations ... +contact.customfield.option.createnew=Créer un nouveau ... +contact.name.label=Nom +contact.phonenumber.label=Mobile +contact.phonenumber.international.warning=Ce nombre n'est pas au format international. Cela peut causer des problèmes pour faire correspondre lees messages aux contacts. +contact.notes.label=Notes +contact.email.label=Email +contact.groups.label=Groupes +contact.notinanygroup.label=Ne fait pas partie d'un groupe +contact.messages.label=Messages +contact.messages.sent={0} messages envoyés +contact.received.messages={0} messages reçus +contact.search.messages=Rechercher parmi les messages +group.rename=Renommer le groupe +group.edit=Modifier le groupe +group.delete=Supprimer le groupe +group.moreactions=Plus actions ... +customfield.validation.prompt=S'il vous plaît inscrire un nom +customfield.name.label=Nom +export.contact.info=Pour exporter des contacts à partir de FrontlineSMS, choisissez le type d'exportation et l'information à inclure dans les données exportées. +export.message.info=Pour exporter des messages de FrontlineSMS, choisissez le type d'exportation et l'information à inclure dans les données exportées. +export.selectformat=Sélectionnez un format de sortie +export.csv=Format CSV pour une utilisation dans le tableau +export.pdf=Format PDF pour l'impression +folder.name.label=Nom +group.delete.prompt=Etes-vous sûr de vouloir supprimer {0}? AVERTISSEMENT: Ceci ne peut être annulé. +layout.settings.header=Paramètres +activities.header=Activités +activities.create=Créer une nouvelle activité +folder.header=Dossiers +folder.create=Créer un nouveau dossier +folder.label=dossier +message.folder.header={0} Dossier +fmessage.trash.actions=Actions de la corbeille ... +fmessage.trash.empty=Vider la corbeille +fmessage.to.label=à +trash.empty.prompt=Tous les messages et les activités dans la poubelle seront supprimés définitivement +fmessage.responses.total={0} Total des réponses +fmessage.label=Message +fmessage.label.multiple={0} messages +poll.prompt=Nommez ce sondage +poll.details.label=Confirmez les détails +poll.message.label=Message +poll.choice.validation.error.deleting.response=Un choix enregistré ne peut pas avoir une valeur vide +poll.alias=Alias +poll.aliases.prompt=Entrez les alias pour les options correspondantes. +poll.aliases.prompt.details=Vous pouvez entrer plusieurs alias pour chaque option, séparés par des virgules. Le premier alias sera envoyé dans le message d'intrsuctions du sondage. +poll.alias.validation.error=Les alias doivent être uniques +poll.sort.label=Tri automatique +poll.autosort.no.description=Messages ne seront pas triées automatiquement. +poll.autosort.description=Trier les messages avec le mot-clé +poll.sort.keyword=mot-clé +poll.sort.by=Trier par +poll.Autoreply.label=Réponse Automatique +poll.Autoreply.none=aucun +poll.recipients.label=Destinataires +poll.recipients.none=Aucun +#TODO embed javascript values +poll.recipients.count=contacts sélectionnés +poll.messages.count=les messages seront envoyés +poll.yes=Oui +poll.no=Non +poll.label=Sondage +poll.description=Envoyer une question et analyser les réponses +poll.messages.sent={0} messages envoyés +poll.response.enabled=Réponse automatique activée +poll.message.edit=Modifier le message à envoyer aux destinataires +poll.message.prompt=Le message suivant sera envoyé aux destinataires du sondage +poll.message.count=Caractères restants 160 (1 SMS) +poll.moreactions.delete=Supprimer le sondage +poll.moreactions.rename=Renommer le sondage +poll.moreactions.edit=Modifier le sondage +poll.moreactions.export=Exporter sondage +#TODO embed javascript values +poll.reply.text=Répondre "{0}" pour Yes, "{1}" pour No. +poll.reply.text1={0} "{1}" pour {2} +poll.reply.text2=S'il vous plaît répondez 'Oui' ou 'Non' +poll.reply.text3=ou +poll.reply.text4={0} {1} +poll.reply.text5=Répondre +poll.reply.text6=S'il vous plaît répondre à +poll.message.send={0} {1} +poll.recipients.validation.error=Sélectionnez les contacts à qui envoyer les messages +frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name={1} {0} "{2}" doivent être uniques +frontlinesms2.Poll.responses.validator.error.frontlinesms2.Poll.responses=Les options de réponse ne peuvent pas être identiques +frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value=Mot clé "{2}" est déjà en cours d'utilisation +wizard.title.new=Nouveau +wizard.fmessage.edit.title=Modifier {0} +popup.title.saved={0} enregistré! +popup.activity.create=Créer une nouvelle activité: Sélectionnez le type de +popup.smartgroup.create=Créer un groupe intelligent +popup.help.title=Aide +smallpopup.customfield.create.title=Créer des champs personnalisés +smallpopup.group.rename.title=Renommer le groupe +smallpopup.group.edit.title=Modifier le groupe +smallpopup.group.delete.title=Supprimer le groupe +smallpopup.fmessage.rename.title=Renommer {0} +smallpopup.fmessage.delete.title=Supprimer {0} +smallpopup.fmessage.export.title=Exporter +smallpopup.delete.prompt=Supprimer {0} ? +smallpopup.delete.many.prompt=Supprimer {0} contacts? +smallpopup.empty.trash.prompt=Vider la corbeille? +smallpopup.messages.export.title=Exporter les Results ({0} messages) +smallpopup.test.message.title=Message de Test +smallpopup.recipients.title=Destinataires +smallpopup.folder.title=Dossier +smallpopup.group.create.title=Groupe +smallpopup.contact.export.title=Exporter +smallpopup.contact.delete.title=Supprimer +contact.selected.many={0} contacts selectionnés +group.join.reply.message=Bienvenue +group.leave.reply.message=Au revoir +fmessage.new.info=Vous avez {0} nouveaux messages. Cliquez pour voir +wizard.quickmessage.title=Message rapide +wizard.messages.replyall.title=Répondre à tous +wizard.send.message.title=Envoyer message +wizard.ok=Ok +wizard.create=Créer +wizard.send=Envoyer +common.settings=Paramètres +common.help=Aide +activity.validation.prompt=S'il-vous-plaît remplir tous les champs obligatoires +Autoreply.blank.keyword=Mot-clé vide. Une réponse sera envoyée à tous les messages entrants +poll.type.prompt=Sélectionnez le type de sondage à créer +poll.question.yes.no=Question à réponse «Oui» ou «Non» +poll.question.multiple=Question à choix multiples (e.g. 'Rouge', 'Blue', 'Vert') +poll.question.prompt=Entrez question +poll.message.none=Ne pas envoyer de message pour ce sondage (recueillir les réponses seulement). +poll.replies.header=Répondre automatiquement aux entrées du sondage (facultatif) +poll.replies.description=Lorsque un message entrant est identifié comme une réponse sondage, envoyez un message à la personne qui a envoyé la réponse. +poll.Autoreply.send=Envoyer une réponse automatique aux réponses du sondage +poll.responses.prompt=Entrer des réponses possibles, (entre 2 et 5) +poll.sort.header=Trier les messages automatiquement en utilisant un mot-clé (facultatif) +poll.sort.description=Si les gens envoient des réponses aux sondages en utilisant un mot-clé, FrontlineSMS peut trier automatiquement les messages sur votre système. +poll.no.automatic.sort=Ne pas trier automatiquement les messages +poll.sort.automatically=Trier automatiquement les messages qui ont le mot-clé suivant +poll.validation.prompt=S'il-vous-plaît remplir tous les champs obligatoires +poll.name.validator.error.name=Les noms du sondage doivent être uniques +pollResponse.value.blank.value=La valeur de la réponse du sondage ne peut pas être vide +poll.alias.validation.error.invalid.alias=Alias non valides. Essayez un, nom, un mot +poll.question=Entrez question +poll.response=Liste de réponse +poll.sort=Tri automatique +poll.reply=Réponse automatique +poll.edit.message=Modifier le message +poll.recipients=Sélectionner les destinataires +poll.confirm=Confirmer +poll.save=Le sondage a été enregistré +poll.messages.queue=Si vous avez choisi d'envoyer des messages avec ce sondage, les messages ont été ajoutés à la file de messages en attente. +poll.messages.queue.status=Il peut prendre un certain temps pour que tous les messages soient envoyés en fonction du nombre de messages et de la connexion réseau. +poll.pending.messages=Pour voir l'état de votre message, ouvrez le dossier 'en attente' des messages. +poll.send.messages.none=Aucun message ne sera envoyé +quickmessage.details.label=Confirmez les détails +quickmessage.message.label=Message +quickmessage.message.none=Aucun +quickmessage.recipient.label=Destinataire +quickmessage.recipients.label=Destinataires +quickmessage.message.count=Caractères restants 160 (1 SMS) +quickmessage.enter.message=Entrer message +quickmessage.select.recipients=Selectionnez les destinataires +quickmessage.confirm=Confirmer +#TODO embed javascript values +quickmessage.recipients.count=contacts sélectionnés +quickmessage.messages.count=les messages seront envoyés +quickmessage.count.label=Nombre de messages: +quickmessage.messages.label=Entrer le message +quickmessage.phonenumber.label=Ajouter le numéro de téléphone: +quickmessage.phonenumber.add=Ajouter +quickmessage.selected.recipients=destinataires sélectionnés +quickmessage.validation.prompt=S'il vous plaît remplir tous les champs obligatoires +fmessage.number.error=Les caractères dans ce champ seront supprimés lors de l'enregistrement +search.filter.label=Limiter la recherche à +search.filter.group=Sélectionnez le groupe +search.filter.activities=Sélectionnez l'activité / dossier +search.filter.messages.all=Tous les messages envoyés et reçus +search.filter.inbox=Seulement les messages reçus +search.filter.sent=Seulement les messages envoyés +search.filter.archive=Inclure archives +search.betweendates.label=Entre les dates +search.header=Rechercher +search.quickmessage=Message rapide +search.export=Exporter les résultats +search.keyword.label=Mot-clé ou une phrase +search.contact.name.label=Nom du contact +search.contact.name=Nom du contact +search.result.header=Résultats +search.moreoptions.label=Plus d'options +settings.general=Général +settings.connections=Téléphones et connexions +settings.logs=Système +settings.general.header=Paramètres> Général +settings.logs.header=Journaux de bord du système +logs.none=Vous n'avez pas les journaux de bord. +logs.content=Message +logs.date=Temps +logs.filter.label=Afficher les journaux debord pour +logs.filter.anytime=tous +logs.filter.1day=dernières 24 heures +logs.filter.3days=les trois derniers jours +logs.filter.7days=les sept derniers jours +logs.filter.14days=les 14 derniers jours +logs.filter.28days=les 28 derniers jours +logs.download.label=Télécharger les journaux de bord du système +logs.download.buttontext=Télécharger les journaux +logs.download.title=Télécharger les journaux à envoyer +logs.download.continue=Continuer +smartgroup.validation.prompt=S'il vous plaît remplir tous les champs obligatoires. Vous ne pouvez spécifier qu'une règle par champ. +smartgroup.info=Pour créer un groupe intelligent, sélectionnez les critères adaptés dont vous avez besoin pour les contacts de ce groupe +smartgroup.contains.label=contient +smartgroup.startswith.label=commence avec +smartgroup.add.anotherrule=Ajouter une autre règle +smartgroup.name.label=Nom +modem.port=Port +modem.description=Description +modem.locked=Verrouillé? +traffic.header=Trafic +traffic.update.chart=Mise à jour graphique +traffic.filter.2weeks=Afficher les dernières deux semaines +traffic.filter.between.dates=entre les dates +traffic.filter.reset=Réinitialiser les filtres +traffic.allgroups=Afficher tous les groupes +traffic.all.folders.activities=Afficher toutes les activités et les dossiers +traffic.sent=Envoyé +traffic.received=Reçu +traffic.total=Total +tab.message=Messages +tab.archive=Archive +tab.contact=Contacts +tab.status=Statut +tab.search=Rechercher +help.info=Ceci est une version une beta, il n'y a pas donc pas d'aide intégrée. S'il vous plaît allez sur les forums d'utilisateurs pour obtenir de l'aide à ce stade +# IntelliSms Fconnection +intellismsfconnection.label=Compte IntelliSms +intellismsfconnection.type.label=Type +intellismsfconnection.name.label=Nom +intellismsfconnection.username.label=Nom d'utilisateur +intellismsfconnection.password.label=Mot de passe +intellismsfconnection.send.label=Utiliser pour l'envoi +intellismsfconnection.receive.label=Utiliser pour recevoir +intellismsfconnection.receiveProtocol.label=Protocole +intellismsfconnection.serverName.label=Nom du serveur +intellismsfconnection.serverPort.label=Port du serveur +intellismsfconnection.emailUserName.label=Nom d'utilisateur +intellismsfconnection.emailPassword.label=Mot de Passe +#Controllers +contact.label=Contact(s) +contact.edited.by.another.user=Un autre utilisateur a mis à jour ce contact alors que vous étiez en train de modifier +contact.exists.prompt=Il existe déjà un contact avec ce numéro +contact.exists.warn=Le contact avec ce numéro existe déjà +contact.view.duplicate=Voir dupliquer +contact.addtogroup.error=Impossible d'ajouter et de supprimer dans le même groupe! +contact.mobile.label=Mobile +contact.email.label=Email +fconnection.label=Fconnection +fconnection.name=Fconnection +fconnection.unknown.type=Type de connexion inconnu: +fconnection.test.message.sent=Message de test envoyé! +announcement.saved=L'annonce a été enregistrée et le message ou les messages ont été mis en attente pour être envoyés +announcement.not.saved=L'annonce n'a pas pu être enregistrée! +announcement.id.exist.not=Impossible de trouver l'annonce avec l'ID {0} +Autoreply.saved=La réponse automatique a été enregistrée ! +Autoreply.not.saved=La réponse automatique ne peut pas être enregistrée ! +report.creation.error=Erreur de création de rapport +export.message.title=Exporter le message FrontlineSMS +export.database.id=DatabaseID +export.message.date.created=Date de création +export.message.text=Texte +export.message.destination.name=Nom de la destination +export.message.destination.mobile=Mobile de la destination +export.message.source.name=Nom de la source +export.message.source.mobile=Mobile de la source +export.contact.title=Exporter les contacts FrontlineSMS +export.contact.name=Nom +export.contact.mobile=Mobile +export.contact.email=Email +export.contact.notes=Notes +export.contact.groups=Groupes +export.messages.name1={0} {1} ({2} messages) +export.messages.name2={0} ({1} messages) +export.contacts.name1={0} groupe ({1} contacts) +export.contacts.name2={0} group intelligent ({1} contacts) +export.contacts.name3=Tous les contacts ({0} contacts) +folder.label=Dossier +folder.archived.successfully=Le dossier a été archivé avec succès! +folder.unarchived.successfully=Le dossier a été désarchivé avec succès! +folder.trashed=Le dossier a été mis à la corbeille! +folder.restored=Le dossier a été restauré! +folder.exist.not=Impossible de trouver le dossier avec l'ID{0} +folder.renamed=Dossier renommé +group.label=Groupe +group.name.label=Nom +group.update.success=Groupe mis à jour avec succès +group.save.fail=L'enregistrement du groupe a échoué +group.delete.fail=Impossible de supprimer le groupe +import.label=Importer +import.backup.label=Importer les données provenant d'une sauvegarde précédente +import.prompt.type=Sélectionnez le type de données à importer +import.contacts=Coordonnées du contacts +import.messages=Détails du message +import.version1.info=Pour importer des données de la version 1, s'il vous plaît exportez les en anglais +import.prompt=Sélectionnez un fichier de données à importer +import.upload.failed=L'envoi du fichier a échoué pour une raison quelconque. +import.contact.save.error=Une erreur a été rencontrée lors de l'enregistrement du contact +import.contact.complete={0} les contacts ont été importés; {1} échoué +import.contact.failed.download=Télécharger les contacts qui ont échoué (CSV) +import.message.save.error=Une erreur a été rencontrée lors de l'enregistrement du message +import.message.complete={0} les messages ont été importés; {1} échoué +many.selected={0} {1}s sélectionné +flash.message.activity.found.not=Cette activité n'a pu être trouvée +flash.message.folder.found.not=Le dossier n'a pas pu être trouvé +flash.message=Message +flash.message.fmessage={0} message(s) +flash.message.fmessages.many={0} messages SMS +flash.message.fmessages.many.one=1 message SMS +fmessage.exist.not=Impossible de trouver un message avec ID {0} +flash.message.poll.queued=Sondage a été enregistré et le message ou les messages(s) ont été mis en attente pour être envoyé(s) +flash.message.poll.saved=Le sondage a été enregistré +flash.message.poll.not.saved=Le sondage ne peut pas être enregistré +system.notification.ok=Ok +system.notification.fail=ECHEC +flash.smartgroup.delete.unable=Impossible de supprimer le groupe intelligent +flash.smartgroup.saved=Groupe Intelligent {0} enregistré +flash.smartgroup.save.failed=L'enregistrement du groupe intelligent a échoué. Les erreurs étaient{0} +smartgroup.id.exist.not=Impossible de trouver groupe intelligent avec id {0} +smartgroup.save.failed=Impossible d'enregistrer le groupe intelligent {0} avec les paramètres {1}-{2} erreurs: {3} +contact.name.label=Nom +contact.phonenumber.label=Numéro de téléphone +searchdescriptor.searching=Recherche en cours +searchdescriptor.all.messages=tous les messages +searchdescriptor.archived.messages=, y compris les messages archivés +searchdescriptor.exclude.archived.messages=, sans les messages archivés +searchdescriptor.only=, seulement {0} +searchdescriptor.between=, entre {0} et {1} +searchdescriptor.from=, à partir de {0} +searchdescriptor.until=, jusqu'à {0} +poll.title={0} sondage +announcement.title={0} annonce +Autoreply.title={0} Réponse automatique +folder.title={0} dossier +frontlinesms.welcome=Bienvenue à FrontlineSMS! \\o/ +failed.pending.fmessages={0} message en attente (s) a échoué. Allez à la section des messages en attente pour voir. +language.label=langue +language.prompt=Changer la langue de l'interface utilisateur FrontlineSMS +frontlinesms.user.support=Soutien aux utilisateurs FrontlineSMS +download.logs.info1=AVERTISSEMENT: L'équipe de FrontlineSMS est incapable de répondre directement aux journaux de bord soumis. Si vous avez fait une demande de support utilisateur, s'il vous plaît vérifiez les fichiers d'aide pour voir si vous pouvez y trouver la réponse. Si ce n'est pas le cas, signalez votre problème via nos forums de soutien aux utilisateurs: +download.logs.info2=Les autres utilisateurs peuvent déjà avoir signalé le même problème et trouvé une solution! Pour continuer et soumettre vos journaux, s'il vous plaît cliquez sur «Continuer» +dynamicfield.contact_name.label=Nom du contact +dynamicfield.contact_number.label=Numero du contact +dynamicfield.keyword.label=Mot-Clé +dynamicfield.message_content.label=Contenu du message +# TextMessage domain +fmessage.queued=Le message a été mis en attente pour être envoyer à {0} +fmessage.queued.multiple=Le message a été mis en attente pour être envoyer à {0} recipients +fmessage.retry.success=Le message a été remis en attente pour être envoyer à {0} +fmessage.retry.success.multiple={0} message(s) a été remis en attente pour être envoyer à +fmessage.displayName.label=Nom +fmessage.text.label=Message +fmessage.date.label=Date +fmessage.to=A: {0} +fmessage.to.multiple=A: {0} destinataires +fmessage.quickmessage=Message rapide +fmessage.archive=Archive +fmessage.activity.archive=Archive {0} +fmessage.unarchive=Non-archive {0} +fmessage.export=Exporter +fmessage.rename=Renommer {0} +fmessage.edit=Modifier {0} +fmessage.delete=Supprimer {0} +fmessage.moreactions=Plus actions ... +fmessage.footer.show=Afficher +fmessage.footer.show.failed=Echoué +fmessage.footer.show.all=Tous +fmessage.footer.show.starred=Favoris +fmessage.archive.back=Retour +fmessage.activity.sentmessage=({0} messages envoyés) +fmessage.failed=échoué +fmessage.header=messages +fmessage.section.inbox=Boîte de réception +fmessage.section.sent=Envoyés +fmessage.section.pending=En attente +fmessage.section.trash=Corbeille +fmessage.addsender=Ajouter aux contacts +fmessage.resend=Renvoyer +fmessage.retry=Réessayer +fmessage.reply=Répondre +fmessage.forward=Transférer +fmessage.unarchive=Désarchiver +fmessage.delete=Supprimer +fmessage.messages.none=Aucun messages ici! +fmessage.selected.none=Aucun message sélectionné +fmessage.move.to.header=Déplacer le message vers ... +fmessage.move.to.inbox=Boîte de réception +fmessage.archive.many=Tout archiver +fmessage.count=1 message +fmessage.count.many={0} messages +fmessage.many=messages +fmessage.delete.many=Tout supprimer +fmessage.reply.many=Répondre à tous +fmessage.restore=Restaurer +fmessage.restore.many=Restaurer +fmessage.retry.many=La nouvelle tentative a échoué +fmessage.selected.many={0} messages selectionnés +fmessage.unarchive.many=Désarchiver tout +# TODO move to poll.* +fmessage.showpolldetails=Afficher le graphique +fmessage.hidepolldetails=Cacher le graphique +# TODO move to search.* +fmessage.search.none=Aucun message trouvé +fmessage.search.description=Démarrer une nouvelle recherche sur la gauche +activity.name=Nom +activity.delete.prompt=Déplacez {0} à la corbeille. Cela consistera à transférer tous les messages associés à la section poubelle. +activity.label=Activité +activity.categorize=Catégoriser réponse. +magicwand.title=Ajoutez des expressions de substitution +folder.create.success=Dossier créé avec succès +folder.create.failed=Impossible de créer le dossier +folder.name.validator.error=Le nom du dossier utilisé +folder.name.blank.error=Le nom du dossier ne peut pas être vide +poll.name.blank.error=Le nom du Sondage ne peut pas être vide +poll.name.validator.error=Nom de sondage utilisé +Autoreply.name.blank.error=Le nom de la réponse automatique ne peut pas être vide +Autoreply.name.validator.error=nom de la réponse automatique utilisé +announcement.name.blank.error=Le nom de l'annonce ne peut pas être vide +announcement.name.validator.error=Nom de l'annonce utilisé +group.name.blank.error=Le nom du groupe ne peut pas être vide +group.name.validator.error=Nom du groupe utilisé +#Jquery Validation messages +jquery.validation.required=Ce champ est obligatoire. +jquery.validation.remote=S'il vous plaît corrigez ce champ +jquery.validation.email=S'il vous plaît entrez une adresse email valide. +jquery.validation.url=S'il vous plaît entrez une URL valide. +jquery.validation.date=S'il vous plaît entrez une date valide. +jquery.validation.dateISO=S'il vous plaît entrez une date valide (ISO). +jquery.validation.number=S'il vous plaît entrez un numero valide. +jquery.validation.digits=S'il vous plaît entrez que des chiffres. +jquery.validation.creditcard=S'il vous plaît entrez un numéro de carte de crédit valide. +jquery.validation.equalto=S'il vous plaît entrez la même valeur à nouveau. +jquery.validation.accept=S'il vous plaît entrez une valeur avec une extension valide. +jquery.validation.maxlength=S'il vous plaît ne pas entrer plus de {0} caractères. +jquery.validation.minlength=S'il vous plaît entrez au moins {0} caractères. +jquery.validation.rangelength=S'il vous plaît entrez une valeur comprise entre {0} et {1} caractères. +jquery.validation.range=S'il vous plaît entrez une valeur comprise entre {0} et {1}. +jquery.validation.max=S'il vous plaît entrez une valeur inférieure ou égale à {0}. +jquery.validation.min=S'il vous plaît entrez une valeur supérieure ou égale à {0}. diff --git a/plugins/frontlinesms-core/grails-app/i18n/messages_in_ID.properties b/plugins/frontlinesms-core/grails-app/i18n/messages_in_ID.properties index 57caf3dd9..114c15557 100644 --- a/plugins/frontlinesms-core/grails-app/i18n/messages_in_ID.properties +++ b/plugins/frontlinesms-core/grails-app/i18n/messages_in_ID.properties @@ -1,751 +1,1065 @@ -# FrontlineSMS English translation by the FrontlineSMS team, Nairobi -language.name=Indonesian - -# General info -app.version.label=Versi - -# Common action imperatives - to be used for button labels and similar -action.ok=OK -action.close=Tutup -action.cancel=Batalkan -action.done=Selesai -action.next=Selanjutnya -action.prev=Sebelumnya -action.back=Kembali -action.create=Buat -action.edit=Sunting -action.rename=Namai Ulang -action.save=Simpan -action.save.all=Simpan Seluruhnya -action.delete=Hapus -action.delete.all=Hapus Seluruhnya -action.send=Kirim -action.export=Ekspor - -# Messages when FrontlineSMS server connection is lost -server.connection.fail.title=Koneksi ke server telah terputus. -server.connection.fail.info=Silakan luncurkan ulang FrontlineSMS, atau tutup jendela ini. - -#Connections: -connection.creation.failed=Tidak dapat melakukan koneksi {0} -connection.route.destroyed=Destroyed route dari {0} ke {1} -connection.route.connecting=Melakukan koneksi... -connection.route.disconnecting=Memutus koneksi... -connection.route.successNotification=Berhasil menciptakan route di {0} -connection.route.failNotification=Gagal menciptakan route pada {1}: {2} [edit] -connection.route.destroyNotification=Putuskan koneksi pada route {0} -connection.test.sent=Tes pesan berhasil terkirim ke to {0} lewat {1} -# Connection exception messages -connection.error.org.smslib.alreadyconnectedexception=Piranti sudah terhubung -connection.error.org.smslib.gsmnetworkregistrationexception=Gagal untuk terhubung dengan jaringan GSM -connection.error.org.smslib.invalidpinexception=Memasukkan PIN yang salah -connection.error.org.smslib.nopinexception=Dibutuhkan PIN tetapi belum dimasukkan -connection.error.java.io.ioexception=Terdapat kesalahan pada port: {0} - -connection.header=Koneksi -connection.list.none=Anda tidak memiliki jaringan yang terkonfigurasi -connection.edit=Sunting Koneksi -connection.delete=Hapus Koneksi -connection.deleted=Koneksi {0} sudah dihapus. -connection.route.create=Buat route -connection.add=Tambahkan koneksi baru -connection.createtest.message.label=Pesan -connection.route.destroy=Hancurkan route -connection.send.test.message=Kirim tes pesan -connection.test.message=Selamat dari FrontlineSMS \\o/ Anda sudah sukses terkonfigurasi dengan {0} untuk mengirim SMS \\o/ -connection.validation.prompt=Harap mengisi seluruh informasi yang diperlukan -connection.select=Pilih tipe koneksi -connection.type=Pilih tipe -connection.details=Masukkan detail -connection.confirm=Konfirmasi -connection.createtest.number=Nomor -connection.confirm.header=Konfirmasi pengaturan -connection.name.autoconfigured=Auto-konfigurasi {0} {1} pada port {2}" - -status.connection.header=Koneksi -status.connection.none=Anda tidak punya koneksi yang terkonfigurasi -status.devises.header=Piranti terdeteksi -status.detect.modems=Mendeteksi modem -status.modems.none=Tidak ada piranti yang sudah terdeteksi. - -connectionstatus.not_connected=Tidak terkoneksi -connectionstatus.connecting=Sedang terkoneksi -connectionstatus.connected=Sudah terkoneksi - -default.doesnt.match.message=Properti [{0}] dari [{1}] dengan [{2}] tidak cocok dengan pola yang disyaratkan [{3}] -default.invalid.url.message=Properti [{0}] dari [{1}] dengan [{2}] bukan URL yang valid -default.invalid.creditCard.message=Properti [{0}] dari [{1}] dengan [{2}] bukan nomor kartu kredit yang valid -default.invalid.email.message=Properti[{0}] dari [{1}] dengan [{2}] bukan alamat email yang valid -default.invalid.range.message=Properti[{0}] dari [{1}] dengan [{2}] tidak berada dalam rentang yang valid dari [{3}] sampai [{4}] -default.invalid.size.message= Properti[{0}] dari [{1}] dengan [{2}] tidak beradad dalam rentang ukuran yang valid dari [{3}] sampai [{4}] -default.invalid.max.message=Properti [{0}] dari [{1}] dengan [{2}] melebihi nilai maksimum [{3}] -default.invalid.min.message=Properti [{0}] dari [{1}] dengan [{2}] kurang dari nilai minumum [{3}] -default.invalid.max.size.message=Properti [{0}] dari [{1}] dengan [{2}] melebihi ukuran maksimum [{3}] -default.invalid.min.size.message=Properti [{0}] dari [{1}] dengan [{2}] kurang dari ukuran minimum [{3}] -default.invalid.validator.message=Properti [{0}] dari [{1}] dengan [{2}] tidak lolos validasi -default.not.inlist.message=Properti [{0}] dari [{1}] dengan [{2}] tidak terdapat dalam daftar [{3}] -default.blank.message=Properti [{0}] dari [{1}] tidak dapat dikosongkan -default.not.equal.message=Properti [{0}] dari [{1}] dengan [{2}] tidak bisa sama dengan [{3}] -default.null.message=Properti [{0}] dari [{1}] tidak bisa nihil -default.not.unique.message=Properti [{0}] dari [{1}] dengan [{2}] harus unik - -default.paginate.prev=Sebelumnya -default.paginate.next=Berikutnya -default.boolean.true=Benar -default.boolean.false=Salah -default.date.format=dd MMMM, yyyy hh:mm -default.number.format=0 - -default.unarchived={0} membuka arsip -default.unarchive.failed=Membuka arsip {0} gagal -default.trashed={0} dibuang -default.restored={0} dikembalikan -default.restore.failed=Tidak dapat mengembalikan {0} dengan id {1} -default.archived={0} berhasil diarsipkan! -default.archived.multiple={0} diarsipkan -default.created={0} dibuat -default.created.message={0} {1} sudah dibuat -default.create.failed=Gagal membuat {0} -default.updated={0} sudah diperbarui -default.update.failed=Gagal memperbarui {0} dengan id {1} -default.updated.multiple= {0} sudah diperbarui -default.updated.message={0} diperbarui -default.deleted={0} dihapus -default.trashed={0} pindahkan ke tempat sampah -default.trashed.multiple={0} pindah ke tempat sampah -default.archived={0} diarsipkan -default.unarchived={0} membuka arsip -default.unarchive.keyword.failed=Membuka arsip {0} gagal. Kata kunci sudah digunakan -default.unarchived.multiple={0} membuka arsip -default.delete.failed=Tidak dapat menghapus {0} dengan id {1} -default.notfound=Tidak dapat menemukan {0} dengan id {1} -default.optimistic.locking.failure=Pengguna lain sedang memperbarui ini {0} ketika Anda sedang menyunting - -default.home.label=Beranda -default.list.label={0} Daftar -default.add.label=Tambahkan {0} -default.new.label=Baru {0} -default.create.label=Buat {0} -default.show.label=Tampilkan {0} -default.edit.label=Sunting {0} -search.clear=Hapus pencarian - -default.button.create.label=Buat -default.button.edit.label=Sunting -default.button.update.label=Perbarui -default.button.delete.label=Hapus -default.button.search.label=Cari -default.button.apply.label=Terapkan -default.button.delete.confirm.message=Anda yakin? - -default.deleted.message={0} dihapus - -# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) -typeMismatch.java.net.URL= {0} harus merupakan URL yang valid -typeMismatch.java.net.URI= {0} harus merupakan URL yang valid -typeMismatch.java.util.Date= {0} harus merupakan tanggal yang valid -typeMismatch.java.lang.Double= {0} harus merupakan nomor yang valid -typeMismatch.java.lang.Integer= {0} harus merupakan nomor yang valid -typeMismatch.java.lang.Long= {0} harus merupakan nomor yang valid -typeMismatch.java.lang.Short= {0} harus merupakan nomor yang valid -typeMismatch.java.math.BigDecimal= {0} harus merupakan nomor yang valid -typeMismatch.java.math.BigInteger= {0} harus merupakan nomor yang valid -typeMismatch.int = {0} harus merupakan nomor yang valid - -# Application specific messages -messages.trash.confirmation=Ini akan mengosongkan tempat sampah dan menghapus pesan secara permanen. Anda yakin mau melanjutkan? -default.created.poll=Poll sudah dibuat! -default.search.label=Bersihkan pencarian -default.search.betweendates.title=Antara tanggal: -default.search.moresearchoption.label=Pilihan pencarian lainnya -default.search.date.format=hari/bulan/tahun -default.search.moreoption.label=Pilihan lainnya - -# SMSLib Fconnection -smslibfconnection.label=Telepon/Modem -smslibfconnection.type.label=Tipe -smslibfconnection.name.label=Nama -smslibfconnection.port.label=Port -smslibfconnection.baud.label=Baud rate -smslibfconnection.pin.label=PIN -smslibfconnection.imsi.label=SIM IMSI -smslibfconnection.serial.label=Serial Piranti # -smslibfconnection.send.label=Gunakan modem HANYA untuk megirim Pesan -smslibfconnection.receive.label =Gunakan modem HANYA untuk menerima Pesan -smslibFconnection.send.validator.error.send=Modem seharusnya digunakan untuk mengirim -smslibFconnection.receive.validator.error.receive=atau menerima pesan - -# Email Fconnection -emailfconnection.label=Email -emailfconnection.type.label=Tipe -emailfconnection.name.label=Nama -emailfconnection.receiveProtocol.label=Protokol -emailfconnection.serverName.label=Nama Server -emailfconnection.serverPort.label=Server Port -emailfconnection.username.label=Nama Pengguna -emailfconnection.password.label=Kata Sandi - -# CLickatell Fconnection -clickatellfconnection.label=Akun Clickatell -clickatellfconnection.type.label=Tipe -clickatellfconnection.name.label=Nama -clickatellfconnection.apiId.label=API ID -clickatellfconnection.username.label=Nama Pengguna -clickatellfconnection.password.label=Kata sandi - -# Messages Tab -message.create.prompt=Masukkan pesan -message.character.count=Tersisa karakter {0} ({1} pesan SMS -message.character.count.warning=Bisa lebih panjang setelah melakukan pertukaran -announcement.label=Pengumuman -announcement.description=Kirim pesan pengumuman dan mengatur balasan -announcement.info1=Pengumuman sudah disimpan dan pesan sudah ditambahkan ke antrian pesan -announcement.info2= Akan memakan waktu beberapa lama untuk mengirikan pesan-pesan ini, tergantung pada jumlah pesan dan jaringan koneksi. -announcement.info3=Untuk melihat status pesan Anda, buka folder pesan "Tertunda" -announcement.info4=Untuk melihat pengumuman, klik saja di menu sebelah kiri -announcement.validation.prompt=Silakan mengisi semua keterangan yang disyaratkan -announcement.select.recipients=Pilih penerima -announcement.confirm=Konfirmasi -announcement.delete.warn=Menghapus {0} PERINGATAN: Anda tidak bisa membatalkan ini! -announcement.prompt=Beri nama pengumuman ini -announcement.confirm.message=Pesan -announcement.details.label=Konfirmasi detail -announcement.message.label=Pesan -announcement.message.none=tidak ada -announcement.recipients.label=Penerima -announcement.create.message=Buat pesan -#TODO embed javascript values -announcement.recipients.count=Kontak dipilih -announcement.messages.count=pesan akan dikirimkan -announcement.moreactions.delete=Hapus pengumuman -announcement.moreactions.rename=Ubah nama pengumuman -announcement.moreactions.edit=Sunting pengumuman -announcement.moreactions.export=Ekspor pengumuman -frontlinesms2.Announcement.name.unique.error.frontlinesms2.Announcement.name={1} {0} "{2}" harus unik - - -archive.inbox=Arsip kotak pesan -archive.sent=Arsip terkirim -archive.activity=Arsip aktivitas -archive.folder=Arsip folder -archive.folder.name=Nama -archive.folder.date=Tanggal -archive.folder.messages=Pesan -archive.folder.none=  Tidak ada folder terarsipkan -archive.activity.name=Nama -archive.activity.type=Tipe -archive.activity.date=Tanggal -archive.activity.messages=Pesan -archive.activity.list.none=  Tidak ada aktivitas terarsipkan -autoreply.enter.keyword=Masukkan kata kunci -autoreply.create.message=Masukkan pesan -autoreply.confirm=Konfirmasi -autoreply.name.label=Pesan -autoreply.details.label=Konfirmasi detail -autoreply.label=Balas secara otomatis -autoreply.keyword.label=Kata kunci -autoreply.description=Membalas pesan yang masuk secara otomatis -autoreply.info= Balasan pesan secara otomatis sudah dibuat, semua pesan yang mengandung kata kunci Anda akan ditambahkan ke dalam aktivitas balasan otomatis ini yang dapat Anda lihat dengan meng-klik di menu sebelah kanan -autoreply.info.warning=Balasan otomatis tanpa kata kunci akan dikirimkan ke semua pesan yang masuk -autoreply.info.note=Catatan: Jika Anda mengarsipkan balasan otomatis, pesan yang masuk tidak akan lagi disortir untuk itu -autoreply.validation.prompt=Harap isi semua keterangan yang disyaratkan -autoreply.message.title=Pesan untuk dikirimkan kembali bagi balasan otomatis ini: -autoreply.keyword.title=Sortir pesan secara otomatis menggunakan Kata Kunci: -autoreply.name.prompt=Namai balasan otomatis ini -autoreply.message.count=0 karakter (1 pesan SMS) -autoreply.moreactions.delete=Hapus balasan otomatis -autoreply.moreactions.rename=Ubah nama balasan otomatis -autoreply.moreactions.edit=Sunting balasan otomatis -autoreply.moreactions.export=Ekspor balasan otomatis -autoreply.all.messages=Jangan gunakan kata kunci (Semua pesan yang masuk akan menerima balasan otomatis ini) -autoreply.text.none=Tidak ada -frontlinesms2.Autoreply.name.unique.error.frontlinesms2.Autoreply.name={1} {0} "{2}" harus unik -frontlinesms2.Autoreply.name.validator.error.frontlinesms2.Autoreply.name=Nama balasan otomatis harus unik -frontlinesms2.Keyword.value.validator.error.frontlinesms2.Autoreply.keyword.value=Keyword "{2}" sudah digunakan -frontlinesms2.Autoreply.autoreplyText.nullable.error.frontlinesms2.Autoreply.autoreplyText=Pesan tidak dapat dikosongkan - -contact.new=Kontak Baru -contact.list.no.contact=Tidak ada kontak di sini! -contact.header=Kontak -contact.all.contacts=Semua kontak -contact.create=Buat kontak baru -contact.groups.header=Grup -contact.create.group=Buat grup baru -contact.smartgroup.header=Grup pintar -contact.create.smartgroup=Buat grup pintar baru -contact.add.to.group=Tambahkan ke grup... -contact.remove.from.group=Keluarkan dari grup -contact.customfield.addmoreinformation=Tambahkan informasi... -contact.customfield.option.createnew=Buat ... baru -contact.name.label=Nama -contact.phonenumber.label=Mobile -contact.phonenumber.international.warning=Nomor ini tidak dalam format internasional. Ini mungkin menyebabkan kesulitan menyampaikan pesan kepada kontak. -contact.notes.label=Catatan -contact.email.label=Email -contact.groups.label=Grup -contact.notinanygroup.label=Bukan bagian dari grup manapun -contact.messages.label=Pesan -contact.sent.messages={0} pesan terkirim -contact.received.messages={0} pesan diterima -contact.search.messages=Cari pesan - -group.rename=Ubah nama grup -group.edit=Sunting grup -group.delete=Hapus grup -group.moreactions=Lainnya... - -customfield.validation.prompt=Harap masukkan sebuah nama -customfield.name.label=Nama -export.contact.info=Untuk mengekspor kontak dari FrontlineSMS, pilih tipe ekspor dan informasi yang perlu disertakan dalam data yang diekspor -export.message.info=Untuk mengekspor pesan dari FrontlineSMS, pilih tipe ekspor dan informasi yang perlu disertakan dalam data yang diekspor -export.selectformat=Pilih format output -export.csv=Format CSV untuk digunakan pada tabel -export.pdf=Format PDF untuk dicetak -folder.name.label=Nama -group.delete.prompt=Anda yakin akan menghapus {0}? PERINGATAN: Hal ini tidak dapat dibatalkan -layout.settings.header=Pengaturan -activities.header=Aktivitas -activities.create=Buat aktivitas baru -folder.header=Folder -folder.create=Buat folder baru -folder.label=folder -message.folder.header={0} Folder -fmessage.trash.actions=Tempat sampah... -fmessage.trash.empty=Kosongkan tempat sampah -fmessage.to.label=Untuk -trash.empty.prompt=Semua pesan dan aktivitas di dalam tempat sampah akan dihapus secara permanen -fmessage.responses.total={0} total balasan -fmessage.label=Pesan -fmessage.label.multiple={0} pesan -poll.prompt=Beri nama poll ini -poll.details.label=Konfirmasikan detail -poll.message.label=Pesan -poll.choice.validation.error.deleting.response=Pilihan yang disimpan tidak bisa kosong -poll.alias=Alias -poll.aliases.prompt=Masukkan alias dari pilihan korespondensi -poll.aliases.prompt.details=Anda dapat memasukkan beberapa alias untuk setiap pilihan, dipisahkan dengan koma. Alias yang pertama akan dikirimkan di pesan instruksi poll -poll.alias.validation.error=Alias harus unik -poll.sort.label=Sortir secara otomatis -poll.autosort.no.description=Pesan tidak akan disortir secara otomatis -poll.autosort.description=Sortir pesan berdasarkan kata kunci -poll.sort.keyword=kata kunci -poll.sort.by=Urutkan berdasarkan -poll.autoreply.label=Balasan otomatis -poll.autoreply.none=tidak ada -poll.recipients.label=Penerima -poll.recipients.none=Tidak ada -#TODO embed javascript values -poll.recipients.count=kontak dipilih -poll.messages.count=pesan akan dikirimkan -poll.yes=Ya -poll.no=Tidak -poll.label=Poll -poll.description=Kirim pertanyaan dan lihat balasannya -poll.messages.sent={0} pesan terkirim -poll.response.enabled=Balasan Otomatis Diaktifkan -poll.message.edit=Sunting pesan untuk dikirim ke penerima -poll.message.prompt=Pesan berikut ini akan dikirimkan kepada penerima poll -poll.message.count=Jumlah karakter tersisa 160 (1 pesan SMS) - -poll.moreactions.delete=Hapus poll -poll.moreactions.rename=Ubah nama poll -poll.moreactions.edit=Sunting poll -poll.moreactions.export=Ekspor poll - -#TODO embed javascript values -poll.reply.text=Balas dengan "{0} {1}" untuk Ya, "{2} {3}" untuk Tidak. -poll.reply.text1={0} "{1} {2}" untuk {3} -poll.reply.text2=Silakan jawab 'Ya' atau 'Tidak' -poll.reply.text3= atau -poll.reply.text4={0} {1} -poll.reply.text5=Balas -poll.reply.text6=Tolong jawab -poll.message.send={0} {1} -poll.recipients.validation.error=Pilih kontak yang hendak dikirimkan pesan -frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name = {1} {0} "{2}" harus unik -frontlinesms2.Poll.responses.validator.error.frontlinesms2.Poll.responses=Pilihan balasan tidak bisa sama persis -frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value = Kata kunci "{2}" sudah digunakan - -wizard.title.new=Baru -wizard.fmessage.edit.title=Sunting {0} -popup.title.saved={0} tersimpan! -popup.activity.create=Buat Aktivitas Baru : Pilih Tipe -popup.smartgroup.create=Buat grup pintar -popup.help.title=Bantuan -smallpopup.customfield.create.title=Buat Data Sendiri -smallpopup.group.rename.title=Ubah nama grup -smallpopup.group.edit.title=Sunting grup -smallpopup.group.delete.title=Hapus grup -smallpopup.fmessage.rename.title=Ubah nama {0} -smallpopup.fmessage.delete.title=Hapus {0} -smallpopup.fmessage.export.title=Ekspor -smallpopup.delete.prompt=Hapus {0} ? -smallpopup.delete.many.prompt=Hapus {0} kontak? -smallpopup.empty.trash.prompt=Kosongkan tempat sampah? -smallpopup.messages.export.title=Ekspor Hasil ({0} pesan) -smallpopup.test.message.title=Tes pesan -smallpopup.recipients.title=Penerima -smallpopup.folder.title=Folder -smallpopup.group.title=Grup -smallpopup.contact.export.title=Ekspor -smallpopup.contact.delete.title=Hapus -contact.selected.many={0} kontak telah dipilih -group.join.reply.message=Selamat datang -group.leave.reply.message=Sampai jumpa -fmessage.new.info=Anda punya {0} pesan baru. Klik untuk membaca -wizard.quickmessage.title=Pesan Cepat -wizard.messages.replyall.title=Balas ke semua -wizard.send.message.title=Kirim pesan -wizard.ok=Ok -wizard.create=Buat -wizard.send=Kirim -common.settings=Pengaturan -common.help=Bantuan - -activity.validation.prompt=Harap isi semua keterangan yang disyaratkan -autoreply.blank.keyword=Kata kunci kosong. Balasan akan dikirimkan ke semua pesan yang masuk - - -poll.type.prompt=Pilih jenis poll yang akan dibuat -poll.question.yes.no=Pertanyaan dengan jawaban 'Ya' atau 'Tidak' -poll.question.multiple=Pertanyaan dengan banyak jawaban (misal: 'Merah', 'Biru', 'Hijau') -poll.question.prompt=Masukkan pertanyaan -poll.message.none=Jangan kirim pesan untuk poll ini (hanya mengumpulkan balasan). -poll.replies.header=Balas respon dari poll secara otomatis (tidak wajib) -poll.replies.description=Ketika pesan masuk diidentifikasi sebagai respon dari poll, kirim pesan kepada orang yang mengirim respon ini. -poll.autoreply.send=Kirim balasan otomatis kepada mereka yang mengirim respon poll -poll.responses.prompt=Masukkan balasan yang memungkinkan (antara 2 dan 5) -poll.sort.header=Sortir pesan secara otomatis menggunakan Kata Kunci (tidak wajib) -poll.sort.description=Jika seseorang mengirimkan respon poll menggunakan Kata Kunci, FrontlineSMS dapat menyortir pesan itu secara otomatis pada sistem Anda -poll.no.automatic.sort=Jangan menyortir pesan secara otomatis -poll.sort.automatically=Sortir pesan yang memiliki Kata Kunci berikut ini secara otomatis -poll.validation.prompt=Harap isi semua keterangan yang disyaratkan -poll.name.validator.error.name=Nama poll harus unik -pollResponse.value.blank.value=Balasan poll tidak bisa dikosongkan -poll.alias.validation.error.invalid.alias=Alias tidak valid. Coba masukkan nama atau kata. -poll.question=Masukkan Pertanyaan -poll.response=Daftar balasan -poll.sort=Penyortiran otomatis -poll.reply=Balasan otomatis -poll.edit.message=Sunting Pesan -poll.recipients=Pilih penerima -poll.confirm=Konfirmasi -poll.save=Poll sudah disimpan! -poll.messages.queue=Jika Anda memutuskan mengirim pesan dengan poll ini, pesan sudah ditambahkan ke antrian pesan. -poll.messages.queue.status=Akan memakan waktu beberapa lama bagi pesan untuk terkirim, tergantung jumlah pesan dan koneksi jaringan. -poll.pending.messages=Untuk melihat status pesan Anda, buka folder pesan 'Tertunda' -poll.send.messages.none=Tidak ada pesan yang akan dikirimkan -quickmessage.details.label=Konfirmasi detail -quickmessage.message.label=Pesan -quickmessage.message.none=Tidak ada -quickmessage.recipient.label=Penerima -quickmessage.recipients.label=Penerima -quickmessage.message.count=Karakter yang tersisa 160 (1 pesan SMS) -quickmessage.enter.message=Masukkan pesan -quickmessage.select.recipients=Pilih penerima -quickmessage.confirm=Konfirmasi -#TODO embed javascript values -quickmessage.recipients.count=Kontak Dipilih -quickmessage.messages.count=pesan akan dikirimkan -quickmessage.count.label=Jumlah Pesan: -quickmessage.messages.label=Masukkan pesan -quickmessage.phonenumber.label=Tambahkan nomor telepon: -quickmessage.phonenumber.add=Tambahkan -quickmessage.selected.recipients=penerima dipilih -quickmessage.validation.prompt=Tolong isi semua keterangan yang disyaratkan - -fmessage.number.error=Karakter dalam bagian ini akan dibuang ketika disimpan -search.filter.label=Batas pencarian ke -search.filter.group=Pilih grup -search.filter.activities=Pilih aktivitas/folder -search.filter.messages.all=Semua yang terkirim dan diterima -search.filter.inbox=Hanya pesan yang diterima -search.filter.sent=Hanya pesan yang terkirim -search.filter.archive=Sertakan Arsip -search.betweendates.label=Antara tanggal -search.header=Cari -search.quickmessage=Pesan cepat -search.export=Ekspor hasil -search.keyword.label=kata kunci atau frasa -search.contact.name.label=Nama kontak -search.contact.name=Nama kontak -search.result.header=Hasil -search.moreoptions.label=Pilihan lainnya -settings.general=Umum -settings.connections=Telepon&koneksi -settings.logs=Sistem -settings.general.header=Pengaturan > Umum -settings.logs.header=Logs System -logs.none=Anda tidak punya logs. -logs.content=Pesan -logs.date=Waktu -logs.filter.label=Tunjukkan logs untuk -logs.filter.anytime=semua waktu -logs.filter.1day=24 jam terakhir -logs.filter.3days=3 hari terakhir -logs.filter.7days=7 hari terakhir -logs.filter.14days=14 hari terakhir -logs.filter.28days=28 hari terakhir -logs.download.label=Unduh sistem logs -logs.download.buttontext=Unduh Logs -logs.download.title=Unduh logs untuk dikirim -logs.download.continue=Lanjutkan - -smartgroup.validation.prompt=Harap isi semua keterangan yang disyaratkan. Anda hanya boleh mengisi satu aturan di setiap keterangan. -smartgroup.info=Untuk menciptakan Grup Pintar, pilih kriteria yang Anda butuhkan untuk disesuaikan dengan kontak dalam grup ini -smartgroup.contains.label=mengandung -smartgroup.startswith.label=diawali dengan -smartgroup.add.anotherrule=Tambahkan aturan lainnya -smartgroup.name.label=Nama - -modem.port=Port -modem.description=Deskripsi -modem.locked=Terkunci? -traffic.header=Traffic -traffic.update.chart=Perbarui grafik -traffic.filter.2weeks=Tunjukkan dua minggu terakhir -traffic.filter.between.dates=Antara tanggal -traffic.filter.reset=Ulangi Filter dari Awal -traffic.allgroups=Tunjukkan semua grup -traffic.all.folders.activities=Tunjukkan semua aktivitas/folder -traffic.sent=Terkirim -traffic.received=Diterima -traffic.total=Total - -tab.message=Pesan -tab.archive=Arsip -tab.contact=Kontak -tab.status=Status -tab.search=Cari - -help.info=Ini adalah versi beta sehingga tidak ada keterangan pertolongan di sini. Silakan menuju forum pengguna untuk mendapatkan pertolongan pada tahap ini. - -# IntelliSms Fconnection -intellismsfconnection.label=Akun IntelliSms -intellismsfconnection.type.label=Tipe -intellismsfconnection.name.label=Nama -intellismsfconnection.username.label=Nama Pengguna -intellismsfconnection.password.label=Kata Sandi - -intellismsfconnection.send.label=Gunakan untuk mengirim -intellismsfconnection.receive.label=Gunakan untuk menerima -intellismsfconnection.receiveProtocol.label=Protokol -intellismsfconnection.serverName.label=Nama Server -intellismsfconnection.serverPort.label=Server Port -intellismsfconnection.emailUserName.label=Nama Pengguna -intellismsfconnection.emailPassword.label=Kata Sandi - -#Controllers -contact.label=Kontak -contact.edited.by.another.user=Pengguna lain sedang memperbarui Kontak ini ketika Anda tengah menyunting -contact.exists.prompt=Sudah ada kontak dengan nomor ini sebelumnya -contact.exists.warn=Kontak dengan nomor ini sudah ada -contact.view.duplicate=Lihat duplikasi -contact.addtogroup.error=Tidak bisa menambah dan mengeluarkan dari grup yang sama! -contact.mobile.label=Telepon genggam -contact.email.label=Email -fconnection.label=Fconnection -fconnection.name=Fconnection -fconnection.unknown.type=Tipe koneksi tidak dikenali: -fconnection.test.message.sent=Tes pesan terkirim! -announcement.saved=Pengumuman sudah disimpan dan pesan sudah dalam antrian untuk dikirim -announcement.not.saved=Pengumuman tidak dapat disimpan! -announcement.id.exist.not=Tidak dapat menemukan pengumuman dengan id {0} -autoreply.saved=Balasan otomatis sudah disimpan! -autoreply.not.saved=Balasan otomatis tidak dapat disimpan! -report.creation.error=Ada kesalahan dalam membuat laporan -export.message.title=Ekspor Pesan FrontlineSMS -export.database.id=DatabaseID -export.message.date.created=Tanggal Dibuat -export.message.text=Teks -export.message.destination.name=Nama yang dituju -export.message.destination.mobile=Nomor telepon genggam yang dituju -export.message.source.name=Nama Pengirim -export.message.source.mobile=Telepon genggam Pengirim -export.contact.title=Ekspor Kontak FrontlineSMS -export.contact.name=Nama -export.contact.mobile=Telepon genggam -export.contact.email=Email -export.contact.notes=Catatan -export.contact.groups=Grup -export.messages.name1={0} {1} ({2} pesan) -export.messages.name2={0} ({1} pesan) -export.contacts.name1={0} grup ({1} kontak) -export.contacts.name2={0} grup pintar ({1} kontak) -export.contacts.name3=Semua kontak ({0} kontak) -folder.label=Folder -folder.archived.successfully=Folder berhasil diarsipkan! -folder.unarchived.successfully=Folder berhasil dibuka! -folder.trashed=Folder sudah dibuang ke tempat sampah! -folder.restored=Folder sudah dikembalikan! -folder.exist.not=Tidak dapat menemukan folder dengan id {0} -folder.renamed=Folder Sudah Diubah Namanya - -group.label=Grup -group.name.label=Nama -group.update.success=Grup berhasil diperbarui -group.save.fail=Gagal menyimpan grup -group.delete.fail=Tidak bisa menghapus grup - -import.label=Impor -import.backup.label=Impor data dari cadangan sebelumnya -import.prompt.type=Pilih tipe data yang akan diimpor -import.contacts=Detail kontak -import.messages=Detail pesan -import.version1.info=Untuk mengimpor data untuk versi 1, harap ekspor dalam bahasa Inggris -import.prompt=Pilih file data yang akan diimpor -import.upload.failed=Gagal mengunggah file karena satu dan lain hal -import.contact.save.error=Menemukan kesalahan dalam menyimpan kontak -import.contact.complete={0} kontak sudah diimpor; {1} gagal -import.contact.failed.download=Unduh kontak yang gagal (CSV) -import.message.save.error=enemukan kesalahan dalam menyimpan pesan -import.message.complete={0} pesan sudah diimpor; {1} gagal - -many.selected = {0} {1} dipilih - -flash.message.activity.found.not=Aktivitas tidak dapat ditemukan -flash.message.folder.found.not=Folder tidak dapat ditemukan -flash.message=Pesan -flash.message.fmessage={0} pesan -flash.message.fmessages.many={0} pesan SMS -flash.message.fmessages.many.one=1 pesan SMS -fmessage.exist.not=Tidak dapat menemukan pesan dengan id {0} -flash.message.poll.queued=Poll sudah disimpan dan pesan sudah berada dalam antrian untuk dikirim -flash.message.poll.saved=Poll sudah disimpan -flash.message.poll.not.saved=Poll tidak bisa disimpan! -system.notification.ok=OK -system.notification.fail=GAGAL -flash.smartgroup.delete.unable=Tidak dapat menghapus grup pintar -flash.smartgroup.saved=Grup pintar {0} disimpan -flash.smartgroup.save.failed=Gagal menyimpan Grup Pintar. Kesalahan {0} -smartgroup.id.exist.not=Tidak dapat menemukan Grup Pintar dengan id {0} -smartgroup.save.failed=Gagal menyimpan grup pintar {0}dengan parameter {1}{2}kesalahan: {3} -contact.name.label=Nama -contact.phonenumber.label=Nomor telepon - -searchdescriptor.searching=Mencari -searchdescriptor.all.messages= semua pesan -searchdescriptor.archived.messages=, termasuk pesan yang diarsipkan -searchdescriptor.exclude.archived.messages=, tanpa pesan yang diarsipkan -searchdescriptor.only=, hanya {0} -searchdescriptor.between=, antara {0} dan {1} -searchdescriptor.from=, dari {0} -searchdescriptor.until=, sampai {0} -poll.title={0} poll -announcement.title={0} pengumuman -autoreply.title={0} balasan otomatis -folder.title={0} folder -frontlinesms.welcome=Selamat datang di FrontlineSMS! \\o/ -failed.pending.fmessages={0} pesan tertunda gagal. Pergi ke pesan tertunda untuk melihat. - -language.label=Bahasa -language.prompt=Ganti bahasa FrontlineSMS di halaman muka pengguna -frontlinesms.user.support=Pertolongan Pengguna FrontlineSMS -download.logs.info1=WARNING: Tim FrontlineSMS tidak bisa langsung menjawab log yang didaftarkan. Jika Anda punya permintaan pertolongan bagi pengguna, silakan klik file "Pertolongan" untuk melihat apakah Anda bisa menemukan jawabannya di sana. Jika tidak, laporkan permasalahan Anda lewat forum pertolongan pengguna kami: -download.logs.info2=Pengguna lain mungkin sudah melaporkan permasalahan yang sama dan mendapatkan solusinya! Untuk melanjutkan dan memasukkan pertanyaan Anda, silakan klik 'Lanjutkan' - -dynamicfield.contact_name.label=Nama Kontak -dynamicfield.contact_number.label=Nomor Kontak -dynamicfield.keyword.label=Kata Kunci -dynamicfield.message_content.label=Isi Pesan - -# Fmessage domain -fmessage.queued=Pesan sudah dalam antrian untuk dikirim ke {0} -fmessage.queued.multiple=Pesan sudah dalam antrian untuk dikirim ke {0} penerima -fmessage.retry.success=Pesan sudah dalam antrian lagi untuk dikirim ke {0} -fmessage.retry.success.multiple={0} pesan sudah dalam antrian lagi untuk dikirim -fmessage.displayName.label=Nama -fmessage.text.label=Pesan -fmessage.date.label=Tanggal -fmessage.to=To: {0} -fmessage.to.multiple=Kepada: {0} penerima -fmessage.quickmessage=Pesan cepat -fmessage.archive=Arsip -fmessage.activity.archive=Arsip {0} -fmessage.unarchive=Buka Arsip {0} -fmessage.export=Ekspor -fmessage.rename=Ubah nama {0} -fmessage.edit=Sunting {0} -fmessage.delete=Hapus {0} -fmessage.moreactions=Lainnya... -fmessage.footer.show=Perlihatkan -fmessage.footer.show.failed=Gagal -fmessage.footer.show.all=Semua -fmessage.footer.show.starred=Ditandai -fmessage.archive.back=Kembali -fmessage.activity.sentmessage=({0} pesan terkirim -fmessage.failed=gagal -fmessage.header=pesan -fmessage.section.inbox=Kotak Masuk -fmessage.section.sent=Terkirim -fmessage.section.pending=Tertunda -fmessage.section.trash=Tempat sampah -fmessage.addsender=Tambahkan ke kontak -fmessage.resend=Kirim ulang -fmessage.retry=Coba lagi -fmessage.reply=Balas -fmessage.forward=Forward -fmessage.unarchive=Buka Arsip -fmessage.delete=Hapus -fmessage.messages.none=Tidak ada pesan di sini! -fmessage.selected.none=Tidak ada pesan dipilih -fmessage.move.to.header=Pindahkan pesan ke... -fmessage.move.to.inbox=Kotak Masuk -fmessage.archive.many=Arsipkan semuanya -fmessage.count=1 pesan -fmessage.count.many={0} pesan -fmessage.many= pesan -fmessage.delete.many=Hapus semuanya -fmessage.reply.many=Balas semuanya -fmessage.restore=Kembalikan -fmessage.restore.many=Kembalikan -fmessage.retry.many=Gagal mencoba lagi -fmessage.selected.many={0} pesan dipilih -fmessage.unarchive.many=Buka semua arsip - -# TODO move to poll.* -fmessage.showpolldetails=Tunjukkan grafik -fmessage.hidepolldetails=Sembunyikan grafik - -# TODO move to search.* -fmessage.search.none=Tidak ada pesan ditemukan. -fmessage.search.description=Mulai pencarian baru di sebelah kiri - -activity.name=Nama -activity.delete.prompt=Pindahkan {0} ke tempat sampah. Ini akan memindahkan seluruh pesan yang terkait ke tempat sampah. -activity.label=Aktivitas -activity.categorize=Kategorikan respon - -magicwand.title=Tambahkan ekspresi pengganti -folder.create.success=Folder berhasil dibuat -folder.create.failed=Tidak bisa membuat folder -folder.name.validator.error=Nama folder sudah digunakan -folder.name.blank.error=Nama folder tidak bisa dikosongkan -poll.name.blank.error=Nama poll tidak bisa dikosongkan -poll.name.validator.error=Nama poll sudah digunakan -autoreply.name.blank.error=Nama balasan otomatis tidak bisa dikosongkan -autoreply.name.validator.error=Nama balasan otomatis sudah digunakan -announcement.name.blank.error=Judul pengumuman tidak bisa dikosongkan -announcement.name.validator.error=Judul pengumuman sudah dipakai -group.name.blank.error=Nama grup tidak bisa dikosongkan -group.name.validator.error=Nama grup sudah dipakai - -#Jquery Validation messages -jquery.validation.required=Data ini wajib diisi. -jquery.validation.remote=Silakan membenarkan data ini. -jquery.validation.email=Tolong masukkan alamat email yang valid. -jquery.validation.url=Silakan masukkan URL yang valid. -jquery.validation.date=Silakan masukkan tanggal yang valid. -jquery.validation.dateISO=Silakan masukkan tanggal yang valid (ISO). -jquery.validation.number=Silakan masukkan angka yang valid. -jquery.validation.digits=Harap hanya memasukkan angka. -jquery.validation.creditcard=Silakan masukkan nomor kartu kredit yang valid. -jquery.validation.equalto=Silakan masukkan angka yang sama kembali. -jquery.validation.accept=Please enter a value with a valid extension. -jquery.validation.maxlength=Silakan masukkan tidal lebih dari {0} karakter. -jquery.validation.minlength=Silakan masukkan sedikitnya {0} karakter. -jquery.validation.rangelength=Silakan masukkan angka antara {0} dan {1} karakter. -jquery.validation.range=Silakan masukkan angka antara {0} dan {1}. -jquery.validation.max=Silakan masukkan angka yang kurang dari atau sama dengan {0}. -jquery.validation.min=Silakan masukkan angka yang lebih besar atau sama dengan {0}. - +# FrontlineSMS English translation by the FrontlineSMS team, Nairobi +language.name=Indonesian +# General info +app.version.label=Versi +# Common action imperatives - to be used for button labels and similar +action.ok=OK +action.close=Tutup +action.cancel=Batalkan +action.done=Selesai +action.next=Selanjutnya +action.prev=Sebelumnya +action.back=Kembali +action.create=Buat +action.edit=Sunting +action.rename=Ubah Nama +action.save=Simpan +action.save.all=Save Selected +action.delete=Hapus +action.delete.all=Delete Selected +action.send=Kirim +action.export=Ekspor +action.view=View +content.loading=Loading... +# Messages when FrontlineSMS server connection is lost +server.connection.fail.title=Koneksi ke server telah terputus. +server.connection.fail.info=Luncurkan ulang FrontlineSMS, atau tutup jendela ini. +#Connections: +connection.creation.failed=Tidak dapat melakukan koneksi {0} +connection.route.disabled=Putuskan route dari {0} ke {1} +connection.route.successNotification=Berhasil membuat route di {0} +connection.route.failNotification=Failed to create connection on {1}: {2} [[[edit]](({0}))]. +connection.route.disableNotification=Putuskan koneksi pada route {0} +connection.route.pauseNotification=Paused connection on {0} +connection.route.resumeNotification=Resumed connection on {0} +connection.test.sent=Tes pesan berhasil terkirim ke {0} lewat {1} +connection.route.exception={1} +# Connection exception messages +connection.error.org.smslib.alreadyconnectedexception=Piranti sudah terhubung +connection.error.org.smslib.gsmnetworkregistrationexception=Gagal terhubung dengan jaringan GSM +connection.error.org.smslib.invalidpinexception=PIN yang dimasukkan salah +connection.error.org.smslib.nopinexception=Dibutuhkan PIN tetapi belum dimasukkan +connection.error.org.smslib.notconnectedexception={0} +connection.error.org.smslib.nosuchportexception=Port tidak ditemukan, atau tidak dapat diakses +connection.error.java.io.ioexception=Terdapat kesalahan pada port: {0} +connection.error.frontlinesms2.camel.exception.invalidapiidexception={0} +connection.error.frontlinesms2.camel.exception.authenticationexception={0} +connection.error.frontlinesms2.camel.exception.insufficientcreditexception={0} +connection.error.serial.nosuchportexception=Port tidak ditemukan +connection.error.org.apache.camel.runtimecamelexception=Tidak dapat terhubung +connection.error.onsave={0} +connection.header=Settings > Connections +connection.list.none=Anda tidak memiliki jaringan yang terkonfigurasi +connection.edit=Sunting Koneksi +connection.delete=Hapus Koneksi +connection.deleted=Koneksi {0} sudah dihapus. +connection.route.enable=Enable +connection.route.retryconnection=Coba lagi +connection.add=Tambahkan koneksi baru +connection.createtest.message.label=Buat tes pesan +connection.route.disable=Putuskan route +connection.send.test.message=Kirim tes pesan +connection.test.message=Selamat dari FrontlineSMS \\o/ Anda sudah sukses terkonfigurasi dengan {0} untuk mengirim SMS \\o/ +connection.validation.prompt=Harap mengisi seluruh informasi yang diperlukan +connection.select=Pilih tipe koneksi +connection.type=Pilih tipe +connection.details=Masukkan rincian +connection.confirm=Konfirmasi +connection.createtest.number=Buat nomor tes +connection.confirm.header=Konfirmasi pengaturan +connection.name.autoconfigured=Konfigurasi otomatis {0} {1} pada port {2}" +status.connection.title=Koneksi +status.connection.manage=Manage your connections +status.connection.none=Anda tidak punya koneksi yang terkonfigurasi +status.devises.header=Piranti terdeteksi +status.detect.modems=Mendeteksi modem +status.modems.none=Tidak ada piranti yang sudah terdeteksi +status.header=Usage Statistics +connectionstatus.connecting=Sedang terkoneksi +connectionstatus.connected=Sudah terkoneksi +connectionstatus.disabled=Putuskan route +connectionstatus.failed=Gagal +connectionstatus.not_connected=Tidak terkoneksi +default.doesnt.match.message=Properti [{0}] dari [{1}] dengan [{2}] tidak cocok dengan pola yang disyaratkan [{3}] +default.invalid.url.message=Properti [{0}] dari [{1}] dengan [{2}] bukan URL yang valid +default.invalid.creditCard.message=Properti [{0}] dari [{1}] dengan [{2}] bukan nomor kartu kredit yang valid +default.invalid.email.message=Properti [{0}] dari [{1}] dengan [{2}] bukan alamat email yang valid +default.invalid.range.message=Properti [{0}] dari [{1}] dengan [{2}] tidak berada dalam rentang yang valid dari [{3}] sampai [{4}] +default.invalid.size.message=Properti [{0}] dari [{1}] dengan [{2}] tidak berada dalam rentang ukuran yang valid dari [{3}] sampai [{4}] +default.invalid.max.message=Properti [{0}] dari [{1}] dengan [{2}] melebihi nilai maksimum [{3}] +default.invalid.min.message=Properti [{0}] dari [{1}] dengan [{2}] kurang dari nilai minumum [{3}] +default.invalid.max.size.message=Properti [{0}] dari [{1}] dengan [{2}] melebihi ukuran maksimum [{3}] +default.invalid.min.size.message=Properti [{0}] dari [{1}] dengan [{2}] kurang dari ukuran minimum [{3}] +default.invalid.validator.message=Properti [{0}] dari [{1}] dengan [{2}] tidak lolos validasi +default.not.inlist.message=Properti [{0}] dari [{1}] dengan [{2}] tidak terdapat dalam daftar [{3}] +default.blank.message=Properti [{0}] dari [{1}] tidak dapat dikosongkan +default.not.equal.message=Properti [{0}] dari [{1}] dengan [{2}] tidak bisa sama dengan [{3}] +default.null.message=Properti [{0}] dari [{1}] tidak bisa nihil +default.not.unique.message=Properti [{0}] dari [{1}] dengan [{2}] harus unik +default.paginate.prev=Sebelumnya +default.paginate.next=Berikutnya +default.boolean.true=Benar +default.boolean.false=Salah +default.date.format=dd MMMM, yyyy hh:mm +default.number.format=0 +default.unarchived={0} membuka arsip +default.unarchive.failed=Membuka arsip {0} gagal +default.restored={0} dikembalikan +default.restore.failed=Tidak dapat mengembalikan {0} dengan id {1} +default.archived.multiple={0} diarsipkan +default.created={0} dibuat +default.created.message={0} {1} sudah dibuat +default.create.failed=Gagal membuat {0} +default.updated={0} sudah diperbarui +default.update.failed=Gagal memperbarui {0} dengan id {1} +default.updated.multiple={0} sudah diperbarui +default.updated.message={0} diperbarui +default.deleted={0} dihapus +default.trashed={0} moved to trash +default.trashed.multiple={0} pindah ke tempat sampah +default.archived={0} archived +default.unarchive.keyword.failed=Membuka arsip {0} gagal. Kata kunci sudah digunakan +default.unarchived.multiple={0} membuka arsip +default.delete.failed=Tidak dapat menghapus {0} dengan id {1} +default.notfound=Tidak dapat menemukan {0} dengan id {1} +default.optimistic.locking.failure=Pengguna lain sedang memperbarui ini {0} ketika Anda sedang menyunting +default.home.label=Beranda +default.list.label={0} Daftar +default.add.label=Tambahkan {0} +default.new.label=Baru {0} +default.create.label=Buat {0} +default.show.label=Tampilkan {0} +default.edit.label=Sunting {0} +search.clear=Hapus pencarian +default.button.create.label=Buat +default.button.edit.label=Sunting +default.button.update.label=Perbarui +default.button.delete.label=Hapus +default.button.search.label=Cari +default.button.apply.label=Terapkan +default.button.delete.confirm.message=Anda yakin? +default.deleted.message={0} dihapus +# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) +typeMismatch.java.net.URL={0} harus merupakan URL yang valid +typeMismatch.java.net.URI={0} harus merupakan URL yang valid +typeMismatch.java.util.Date={0} harus merupakan tanggal yang valid +typeMismatch.java.lang.Double={0} harus merupakan nomor yang valid +typeMismatch.java.lang.Integer={0} harus merupakan nomor yang valid +typeMismatch.java.lang.Long={0} harus merupakan nomor yang valid +typeMismatch.java.lang.Short={0} harus merupakan nomor yang valid +typeMismatch.java.math.BigDecimal={0} harus merupakan nomor yang valid +typeMismatch.java.math.BigInteger={0} harus merupakan nomor yang valid +typeMismatch.int={0} harus merupakan nomor yang valid +# Application specific messages +messages.trash.confirmation=Ini akan mengosongkan tempat sampah dan menghapus pesan secara permanen. Anda yakin mau melanjutkan? +default.created.poll=Poll sudah dibuat! +default.search.label=Bersihkan pencarian +default.search.betweendates.title=Antara tanggal: +default.search.moresearchoption.label=Pilihan pencarian lainnya +default.search.date.format=hari/bulan/tahun +default.search.moreoption.label=Pilihan lainnya +# SMSLib Fconnection +smslib.label=Telepon/Modem +smslib.type.label=Tipe +smslib.name.label=Nama +smslib.manufacturer.label=Perusahaan +smslib.model.label=Model +smslib.port.label=Port +smslib.baud.label=Baud rate +smslib.pin.label=PIN +smslib.imsi.label=SIM IMSI +smslib.serial.label=Serial Piranti # +smslib.sendEnabled.label=Use for sending +smslib.receiveEnabled.label=Use for receiving +smslibFconnection.sendEnabled.validator.error.send=Modem should be used for sending +smslibFconnection.receiveEnabled.validator.error.receive=or receiving messages +smslib.description=Sambungkan ke USB, serial, modem bluetooth atau telepon +smslib.global.info=FrontlineSMS akan melakukan konfigurasi secara otomatis dengan sambungan modem atau telepon, tetapi anda bisa mengatur konfigurasi secara manual disini +# Email Fconnection +email.label=Email +email.type.label=Tipe +email.name.label=Nama +email.receiveProtocol.label=Protokol +email.serverName.label=Nama Server +email.serverPort.label=Port Server +email.username.label=Nama Pengguna +email.password.label=Kata Sandi +# CLickatell Fconnection +clickatell.label=Akun Clickatell +clickatell.type.label=Tipe +clickatell.name.label=Nama +clickatell.apiId.label=API ID +clickatell.username.label=Nama Pengguna +clickatell.password.label=Kata sandi +clickatell.sendToUsa.label=Kirim ke AS +clickatell.fromNumber.label=Dari Nomor +clickatell.description=Kirim dan terima pesan melalui akun Clickatell +clickatell.global.info=Anda harus mengatur konfigurasi dengan akun Clickatell (www.clickatell.com). +clickatellFconnection.fromNumber.validator.invalid=Dibutuhkan 'Nomor Dari' untuk mengirimkan pesan ke Amerika Serikat +# TODO: Change markup below to markdown +clickatell.info-local=In order to set up a Clickatell connection, you must first have a Clickatell account. If you do not have one, please go to the Clickatell site and register for a 'Developer's Central Account'. It is free to sign up for test messages, and the process should take less that 5 minutes.

Once you have an active Clickatell account, you will need to 'Create a Connection (API ID)' from the front page. First, select 'APIs,' then select 'Set up a new API.' From there, choose 'add HTTP API' with the default settings, then enter the relevant details below.

The 'Name' field is just for your own reference for your Frontline account, and not related to the Clickatell API, e.g. 'My local message connection'. +clickatell.info-clickatell=The following details should be copied and pasted directly from the Clickatell HTTP API screen. +#Nexmo Fconnection +nexmo.label=Nexmo +nexmo.type.label=Nexmo connection +nexmo.name.label=Nama +nexmo.api_key.label=PIN API: +nexmo.api_secret.label=API secret +nexmo.fromNumber.label=Dari Nomor +nexmo.description=Kirim dan terima pesan melalui akun Clickatell +nexmo.receiveEnabled.label=Fungsi menerima diaktifkan +nexmo.sendEnabled.label=Fungsi mengirim diaktifkan +# Smssync Fconnection +smssync.label=SMSSync +smssync.name.label=Nama +smssync.type.label=Tipe +smssync.receiveEnabled.label=Fungsi menerima diaktifkan +smssync.sendEnabled.label=Fungsi mengirim diaktifkan +smssync.secret.label=Rahasia +smssync.timeout.label=Timeout (mins) +smssync.description=Gunakan HP Android yang sudah terinstal dengan app SMSSync untuk mengirim dan menerima SMS dengan FrontlineSMS +smssync.field.secret.info=Di app anda, atur 'rahasia' sesuai dengan kolom ini +smssync.global.info=Unduh app SMSSync dari smssync.ushahidi.com +smssync.timeout=The Android phone associated with "{0}" has not contacted your Frontline account for {1} minute(s) [edit] +smssync.info-setup=Frontline products enable you to send and receive messages through your Android phone. In order to do this you will need to:\n\n1. Input a 'Secret' and name your connection. A secret is simply a password of your choice.\n2. Download and install [SMSSync from the Android App store](https://play.google.com/store/apps/details?id=org.addhen.smssync&hl=en) to your Android phone\n3. Once you have created this connection, you can create a new Sync URL within SMSSync on your Android phone by entering the connection URL (generated by your Frontline product and displayed on the next page) and your chosen secret. See [The SMSSync Site](http://smssync.ushahidi.com/howto) for more help. +smssync.info-timeout=If SMSSync does not contact your Frontline product for a certain duration (default 60 minutes), your queued messages will NOT be sent, and you will see a notification that the messages failed to send. Select this duration below: +smssync.info-name=Finally, you should name your SMSSync connection with a name of your choice, e.g. 'Bob's work Android'. +# Messages Tab +message.create.prompt=Masukkan pesan +message.character.count=Tersisa karakter {0} ({1} pesan SMS +message.character.count.warning=Bisa lebih panjang setelah melakukan pertukaran +message.header.inbox=Kotak pesan +message.header.sent=Terkirim +message.header.pending=Tertunda +message.header.trash=Tempat sampah +message.header.folder=Folder +message.header.activityList=DaftarAktivitas +message.header.folderList=DaftarFolder +announcement.label=Pengumuman +announcement.description=Kirim pesan pengumuman dan mengatur balasan +announcement.info1=Pengumuman sudah disimpan dan pesan sudah ditambahkan ke antrian pesan +announcement.info2=Akan memakan waktu beberapa lama untuk mengirimkan pesan-pesan ini, tergantung pada jumlah pesan dan jaringan koneksi. +announcement.info3=Untuk melihat status pesan Anda, buka folder pesan "Tertunda" +announcement.info4=Untuk melihat pengumuman, klik menu di sebelah kiri +announcement.validation.prompt=Silakan mengisi semua keterangan yang disyaratkan +announcement.select.recipients=Pilih penerima +announcement.confirm=Konfirmasi +announcement.delete.warn=Menghapus {0} PERINGATAN: Anda tidak bisa membatalkan! +announcement.prompt=Beri nama pengumuman ini +announcement.confirm.message=Pesan +announcement.details.label=Konfirmasi rincian +announcement.message.label=Pesan +announcement.message.none=tidak ada +announcement.recipients.label=Penerima +announcement.create.message=Buat pesan +#TODO embed javascript values +announcement.recipients.count=Kontak dipilih +announcement.messages.count=Pesan akan dikirimkan +announcement.moreactions.delete=Hapus pengumuman +announcement.moreactions.rename=Ubah nama pengumuman +announcement.moreactions.edit=Sunting pengumuman +announcement.moreactions.export=Ekspor pengumuman +frontlinesms2.Announcement.name.unique.error.frontlinesms2.Announcement.name={1} {0} "{2}" harus unik +archive.inbox=Arsip pesan masuk +archive.sent=Arsip terkirim +archive.activity=Arsip aktivitas +archive.folder=Arsip folder +archive.folder.name=Nama +archive.folder.date=Tanggal +archive.folder.messages=Pesan +archive.folder.none=  Tidak ada folder terarsipkan +archive.activity.name=Nama +archive.activity.type=Tipe +archive.activity.date=Tanggal +archive.activity.messages=Pesan +archive.activity.list.none=  Tidak ada aktivitas terarsipkan +archive.header=Arsip +autoreply.enter.keyword=Masukkan kata kunci +autoreply.create.message=Masukkan pesan +activity.autoreply.sort.description=Jika pengunjung mengirimkan pesan diawali dengan kata kunci tertentu, FrontlineSMS dapat memprosesnya secara otomatis di dalam sistem anda. +activity.autoreply.disable.sorting.description=Pesan tidak akan dipindahkan secara otomatis ke dalam aktivitas ini dan dibalas +autoreply.confirm=Konfirmasi +autoreply.name.label=Pesan +autoreply.details.label=Konfirmasi detail +autoreply.label=Balas secara otomatis +autoreply.keyword.label=Kata kunci +autoreply.description=Membalas pesan yang masuk secara otomatis +autoreply.info=Balasan pesan secara otomatis sudah dibuat, semua pesan yang mengandung kata kunci Anda akan ditambahkan ke dalam aktivitas balasan otomatis ini yang dapat Anda lihat dengan meng-klik di menu sebelah kanan +autoreply.info.warning=Balasan otomatis tanpa kata kunci akan dikirimkan ke semua pesan yang masuk +autoreply.info.note=Catatan: Jika Anda mengarsipkan balasan otomatis, pesan yang masuk tidak akan disortir lagi untuk itu +autoreply.validation.prompt=Harap isi semua keterangan yang disyaratkan +autoreply.message.title=Pesan untuk dikirimkan kembali bagi balasan otomatis ini: +autoreply.keyword.title=Sortir pesan secara otomatis menggunakan Kata Kunci: +autoreply.name.prompt=Namai balasan otomatis ini +autoreply.message.count=0 karakter (1 pesan SMS) +autoreply.moreactions.delete=Hapus balasan otomatis +autoreply.moreactions.rename=Ubah nama balasan otomatis +autoreply.moreactions.edit=Sunting balasan otomatis +autoreply.moreactions.export=Ekspor balasan otomatis +autoreply.all.messages=Jangan gunakan kata kunci (Semua pesan yang masuk akan menerima balasan otomatis ini) +autoreply.text.none=Tidak ada +frontlinesms2.Autoreply.name.unique.error.frontlinesms2.Autoreply.name={1} {0} "{2}" harus unik +frontlinesms2.Autoreply.name.validator.error.frontlinesms2.Autoreply.name=Nama balasan otomatis harus unik +frontlinesms2.Keyword.value.validator.error.frontlinesms2.Autoreply.keyword.value=Keyword "{2}" sudah digunakan +frontlinesms2.Autoreply.autoreplyText.nullable.error.frontlinesms2.Autoreply.autoreplyText=Pesan tidak dapat dikosongkan +autoforward.title={0} +autoforward.label=Teruskan Otomatis +autoforward.description=Pesan masuk akan diteruskan secara otomatis ke kontak +autoforward.recipientcount.current=Saat ini {0} penerima +autoforward.create.message=Masukkan pesan +autoforward.confirm=Konfirmasi +autoforward.recipients=Penerima +autoforward.name.prompt=Nama Teruskan Otomatis ini +autoforward.details.label=Rincian konfirmasi +autoforward.keyword.label=Kata kunci +autoforward.name.label=Pesan +autoforward.contacts=Kontak +autoforward.groups=Grup +autoforward.info=Teruskan Otomatis sudah dibuat, setiap pesan mengandung kata kunci akan dimasukkan ke aktivitas ini. Silahkan klik kanan di menu untuk melihatnya. +autoforward.info.warning=Apabila Teruskan Otomatis tidak menggunakan kata kunci maka semua pesan masuk akan diteruskan. +autoforward.info.note=Catatan: Jika anda mengarsipkan Teruskan Otomatis, pesan masuk tidak akan disortir lagi. +autoforward.save=Teruskan Otomatis sudah disimpan! +autoforward.save.success=Meneruskan Otomatis sudah disimpan! +autoforward.global.keyword=Tidak ada (semua pesan masuk akan diproses) +autoforward.disabled.keyword=Tidak ada (sortir otomatis dimatikan) +autoforward.keyword.none.generic=Tidak ada +autoforward.groups.none=Tidak ada +autoforward.contacts.none=Tidak ada +autoforward.message.format=Pesan +contact.new=Kontak Baru +contact.list.no.contact=Tidak ada kontak di sini! +contact.header=Kontak +contact.header.group=Kontak >> {0} +contact.all.contacts=Semua kontak +contact.create=Buat kontak baru +contact.groups.header=Grup +contact.create.group=Buat grup baru +contact.smartgroup.header=Grup pintar +contact.create.smartgroup=Buat grup pintar baru +contact.add.to.group=Tambahkan ke grup... +contact.remove.from.group=Keluarkan dari grup +contact.customfield.addmoreinformation=Tambahkan informasi... +contact.customfield.option.createnew=Buat ... baru +contact.name.label=Nama +contact.phonenumber.label=Nomor Telepon +contact.notes.label=Catatan +contact.email.label=Email +contact.groups.label=Grup +contact.notinanygroup.label=Bukan bagian dari grup manapun +contact.messages.label=Pesan +contact.messages.sent={0} pesan terkirim +contact.received.messages={0} pesan diterima +contact.search.messages=Cari pesan +contact.select.all=Pilih semua +contact.search.placeholder=Search your contacts, or enter phone numbers +contact.search.contact=Kontak +contact.search.smartgroup=Grup pintar +contact.search.group=Grup +contact.search.address=Tambahkan nomor telepon: +contact.not.found=Kontak tidak ditemukan +group.not.found=Grup tidak ditemukan +smartgroup.not.found=Grup Pintar tidak ditemukan +group.rename=Ubah nama grup +group.edit=Sunting grup +group.delete=Hapus grup +group.moreactions=Lainnya... +customfield.validation.prompt=Harap masukkan sebuah nama +customfield.validation.error=Nama sudah dipakai +customfield.name.label=Nama +export.contact.info=Untuk mengekspor kontak dari FrontlineSMS, pilih tipe ekspor dan informasi yang perlu disertakan dalam data yang diekspor +export.message.info=Untuk mengekspor pesan dari FrontlineSMS, pilih tipe ekspor dan informasi yang perlu disertakan dalam data yang diekspor +export.selectformat=Pilih format output +export.csv=Format CSV untuk digunakan pada tabel +export.pdf=Format PDF untuk dicetak +folder.name.label=Nama +group.delete.prompt=Anda yakin akan menghapus {0}? PERINGATAN: Langkah ini tidak dapat dibatalkan +layout.settings.header=Pengaturan +activities.header=Aktivitas +activities.create=Buat aktivitas baru +folder.header=Folder +folder.create=Buat folder baru +folder.label=folder +message.folder.header={0} Folder +fmessage.trash.actions=Tempat sampah... +fmessage.trash.empty=Kosongkan tempat sampah +fmessage.to.label=Untuk +trash.empty.prompt=Semua pesan dan aktivitas di dalam tempat sampah akan dihapus secara permanen +fmessage.responses.total={0} total balasan +fmessage.label=Pesan +fmessage.label.multiple={0} pesan +poll.prompt=Beri nama poll ini +poll.details.label=Konfirmasikan rincian +poll.message.label=Pesan +poll.choice.validation.error.deleting.response=Pilihan yang disimpan tidak bisa kosong +poll.alias=Alias +poll.keywords=Kata kunci +poll.aliases.prompt=Masukkan alias dari pilihan korespondensi +poll.keywords.prompt.details=Kata kunci terpopuler akan digunakan untuk menamai poll dan disertakan di dalam instruksi pesan poll. Setiap respon bisa memiliki kata kunci alternatif. +poll.keywords.prompt.more.details=Anda boleh memasukkan beberapa kata kunci terpopuler dipisahkan dengan tanda koma serta balasannya. Jika kata kunci terpopuler dikosongkan, maka kata kunci balasan harus unik disemua aktivitas. +poll.keywords.response.label=Kata kunci balasan +poll.response.keyword=Masukkan kaca kunci balasan +poll.set.keyword=Masukkan kata kunci terpopuler +poll.keywords.validation.error=Kata kunci harus unik +poll.sort.label=Sortir secara otomatis +poll.autosort.no.description=Pesan tidak akan disortir secara otomatis +poll.autosort.description=Sortir pesan berdasarkan kata kunci +poll.sort.keyword=kata kunci +poll.sort.toplevel.keyword.label=Kata kunci terpopuler (pilihan) +poll.sort.by=Urutkan berdasarkan +poll.autoreply.label=Balasan otomatis +poll.autoreply.none=Tidak ada +poll.recipients.label=Penerima +poll.recipients.none=Tidak ada +poll.toplevelkeyword=Kata kunci terpopuler +poll.sort.example.toplevel=contoh TIM +poll.sort.example.keywords.A=contoh A, AMAZING +poll.sort.example.keywords.B=contoh B, BEAUTIFUL +poll.sort.example.keywords.C=contoh C, COURAGEOUS +poll.sort.example.keywords.D=contoh D, DELIGHTFUL +poll.sort.example.keywords.E=contoh E, EXEMPLARY +poll.sort.example.keywords.yn.A=contoh YES, YAP +poll.sort.example.keywords.yn.B=contoh No, NOP +#TODO embed javascript values +poll.recipients.count=kontak dipilih +poll.messages.count=pesan akan dikirimkan +poll.yes=Ya +poll.no=Tidak +poll.label=Poll +poll.description=Kirim pertanyaan dan lihat balasannya +poll.messages.sent={0} pesan terkirim +poll.response.enabled=Balasan Otomatis Diaktifkan +poll.message.edit=Sunting pesan untuk dikirim ke penerima +poll.message.prompt=Pesan berikut ini akan dikirimkan kepada penerima poll +poll.message.count=Jumlah karakter tersisa 160 (1 pesan SMS) +poll.moreactions.delete=Hapus poll +poll.moreactions.rename=Ubah nama poll +poll.moreactions.edit=Sunting poll +poll.moreactions.export=Ekspor poll +folder.moreactions.delete=Hapus folder +folder.moreactions.rename=Ubah nama folder +folder.moreactions.export=Ekspor folder +#TODO embed javascript values +poll.reply.text=Balas dengan "{0}" untuk Ya, "{1}" untuk Tidak. +poll.reply.text1={0} "{1}" untuk {2} +poll.reply.text2=Silakan jawab 'Ya' atau 'Tidak' +poll.reply.text3=atau +poll.reply.text5=Balas +poll.reply.text6=Tolong jawab +poll.message.send={0} {1} +poll.recipients.validation.error=Pilih kontak yang hendak dikirimi pesan +frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name={1} {0} "{2}" harus unik +frontlinesms2.Poll.responses.validator.error.frontlinesms2.Poll.responses=Pilihan balasan tidak bisa sama persis +frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value=Kata kunci "{2}" sudah digunakan +wizard.title.new=Baru +wizard.fmessage.edit.title=Sunting {0} +popup.title.saved={0} tersimpan! +popup.activity.create=Buat Aktivitas Baru : Pilih Tipe +popup.smartgroup.create=Buat grup pintar +popup.help.title=Bantuan +smallpopup.customfield.create.title=Buat Data Sendiri +smallpopup.group.rename.title=Ubah nama grup +smallpopup.group.edit.title=Sunting grup +smallpopup.group.delete.title=Hapus grup +smallpopup.fmessage.rename.title=Ubah nama {0} +smallpopup.fmessage.delete.title=Hapus {0} +smallpopup.fmessage.export.title=Ekspor +smallpopup.delete.prompt=Hapus {0} ? +smallpopup.delete.many.prompt=Hapus {0} kontak? +smallpopup.empty.trash.prompt=Kosongkan tempat sampah? +smallpopup.messages.export.title=Ekspor Hasil ({0} pesan) +smallpopup.test.message.title=Tes pesan +smallpopup.recipients.title=Penerima +smallpopup.folder.title=folder +smallpopup.contact.export.title=Ekspor +smallpopup.contact.delete.title=Hapus +contact.selected.many={0} kontak telah dipilih +group.join.reply.message=Selamat datang +group.leave.reply.message=Sampai jumpa +fmessage.new.info=Anda punya {0} pesan baru. Klik untuk membaca +wizard.quickmessage.title=Send Message +wizard.messages.replyall.title=Balas ke semua +wizard.send.message.title=Kirim pesan +wizard.ok=Ok +wizard.create=Buat +wizard.save=Simpan +wizard.send=Kirim +common.settings=Pengaturan +common.help=Bantuan +validation.nospaces.error=Kata kunci tidak boleh menggunakan spasi +activity.validation.prompt=Harap isi semua keterangan yang disyaratkan +validator.invalid.name=Another activity exists with the name {2} +autoreply.blank.keyword=Kata kunci kosong. Balasan akan dikirimkan ke semua pesan yang masuk +poll.type.prompt=Pilih jenis poll yang akan dibuat +poll.question.yes.no=Pertanyaan dengan jawaban 'Ya' atau 'Tidak' +poll.question.multiple=Pertanyaan dengan banyak jawaban (misal: 'Merah', 'Biru', 'Hijau') +poll.question.prompt=Masukkan pertanyaan +poll.message.none=Jangan kirim pesan untuk poll ini (hanya mengumpulkan balasan). +poll.replies.header=Balas respon dari poll secara otomatis (tidak wajib) +poll.replies.description=Ketika pesan masuk diidentifikasi sebagai respon dari poll, kirim pesan kepada orang yang mengirim respon ini. +poll.autoreply.send=Kirim balasan otomatis kepada mereka yang mengirim respon poll +poll.responses.prompt=Masukkan balasan yang memungkinkan (antara 2 dan 5) +poll.sort.header=Sortir pesan secara otomatis menggunakan Kata Kunci (tidak wajib) +poll.sort.enter.keywords=Masukkan kata kunci untuk poll dan jawabannya +poll.sort.description=Jika seseorang mengirimkan respon poll menggunakan Kata Kunci, FrontlineSMS dapat menyortir pesan itu secara otomatis pada sistem Anda +poll.no.automatic.sort=Jangan menyortir pesan secara otomatis +poll.sort.automatically=Sortir pesan yang memiliki Kata Kunci berikut ini secara otomatis +poll.validation.prompt=Harap isi semua keterangan yang disyaratkan +poll.name.validator.error.name=Nama poll harus unik +pollResponse.value.blank.value=Balasan poll tidak bisa dikosongkan +poll.keywords.validation.error.invalid.keyword=Kata kunci salah. Coba masukkan sebuah, nama, kata +poll.question=Masukkan Pertanyaan +poll.response=Daftar balasan +poll.sort=Penyortiran otomatis +poll.reply=Balasan otomatis +poll.edit.message=Sunting Pesan +poll.recipients=Pilih penerima +poll.confirm=Konfirmasi +poll.save=Poll sudah disimpan! +poll.save.success={0} Poll telah disimpan +poll.messages.queue=Jika Anda memutuskan mengirim pesan dengan poll ini, pesan sudah ditambahkan ke antrian pesan. +poll.messages.queue.status=Akan memakan waktu beberapa lama bagi pesan untuk terkirim, tergantung jumlah pesan dan koneksi jaringan. +poll.pending.messages=Untuk melihat status pesan Anda, buka folder pesan 'Tertunda' +poll.send.messages.none=Tidak ada pesan yang akan dikirimkan +quickmessage.details.label=Konfirmasi rincian +quickmessage.message.label=Pesan +quickmessage.message.none=Tidak ada +quickmessage.recipient.label=Penerima +quickmessage.recipients.label=Penerima +quickmessage.message.count=Karakter yang tersisa 160 (1 pesan SMS) +quickmessage.enter.message=Masukkan pesan +quickmessage.select.recipients=Pilih penerima +quickmessage.confirm=Konfirmasi +#TODO embed javascript values +quickmessage.recipients.count=Kontak Dipilih +quickmessage.messages.count=pesan akan dikirimkan +quickmessage.count.label=Jumlah Pesan: +quickmessage.messages.label=Masukkan pesan +quickmessage.phonenumber.label=Tambahkan nomor telepon: +quickmessage.phonenumber.add=Tambahkan +quickmessage.selected.recipients=penerima dipilih +quickmessage.validation.prompt=Tolong isi semua keterangan yang disyaratkan +fmessage.number.error=Non-numeric characters in this field will be removed when saved +search.filter.label=Batas pencarian ke +search.filter.group=Pilih grup +search.filter.activities=Pilih aktivitas/folder +search.filter.messages.all=Semua yang terkirim dan diterima +search.filter.inbox=Hanya pesan yang diterima +search.filter.sent=Hanya pesan yang terkirim +search.filter.archive=Sertakan Arsip +search.betweendates.label=Antara tanggal +search.header=Cari +search.quickmessage=Send message +search.export=Ekspor hasil +search.keyword.label=kata kunci atau frasa +search.contact.name.label=Nama kontak +search.contact.name=Nama kontak +search.result.header=Hasil +search.moreoptions.label=Pilihan lainnya +settings.general=Umum +settings.porting=Import and Export +settings.connections=Telepon&koneksi +settings.logs=Sistem +settings.general.header=Pengaturan > Umum +settings.logs.header=Settings > System Logs +logs.none=Anda tidak punya logs. +logs.content=Pesan +logs.date=Waktu +logs.filter.label=Tunjukkan logs untuk +logs.filter.anytime=semua waktu +logs.filter.days.1=last 24 hours +logs.filter.days.3=last 3 days +logs.filter.days.7=last 7 days +logs.filter.days.14=last 14 days +logs.filter.days.28=last 28 days +logs.download.label=Unduh sistem logs +logs.download.buttontext=Unduh Logs +logs.download.title=Unduh logs untuk dikirim +logs.download.continue=Lanjutkan +smartgroup.validation.prompt=Harap isi semua keterangan yang disyaratkan. Anda hanya boleh mengisi satu aturan di setiap keterangan. +smartgroup.info=Untuk menciptakan Grup Pintar, pilih kriteria yang Anda butuhkan untuk disesuaikan dengan kontak dalam grup ini +smartgroup.contains.label=mengandung +smartgroup.startswith.label=diawali dengan +smartgroup.add.anotherrule=Tambahkan aturan lainnya +smartgroup.name.label=Nama +modem.port=Port +modem.description=Deskripsi +modem.locked=Terkunci? +traffic.header=Traffic +traffic.update.chart=Perbarui grafik +traffic.filter.2weeks=Tunjukkan dua minggu terakhir +traffic.filter.between.dates=Antara tanggal +traffic.filter.reset=Ulangi Filter dari Awal +traffic.allgroups=Tunjukkan semua grup +traffic.all.folders.activities=Tunjukkan semua aktivitas/folder +traffic.sent=Terkirim +traffic.received=Diterima +traffic.total=Total +tab.message=Pesan +tab.archive=Arsip +tab.contact=Kontak +tab.status=Status +tab.search=Cari +help.info=Ini adalah versi beta sehingga tidak ada keterangan pertolongan di sini. Silakan menuju forum pengguna untuk mendapatkan pertolongan pada tahap ini. +help.notfound=This help file is not yet available, sorry. +# IntelliSms Fconnection +intellisms.label=Akun Intellisms +intellisms.type.label=Tipe +intellisms.name.label=Nama +intellisms.username.label=Nama Pengguna +intellisms.password.label=Kata Sandi +intellisms.sendEnabled.label=Use for sending +intellisms.receiveEnabled.label=Use for receiving +intellisms.receiveProtocol.label=Protokol +intellisms.serverName.label=Nama Server +intellisms.serverPort.label=Port Server +intellisms.emailUserName.label=Nama Pengguna +intellisms.emailPassword.label=Kata Sandi +intellisms.description=Kirim dan terima pesan melalui akun Intellisms +intellisms.global.info=Anda harus mengonfigurasi akun dengan Intellisms (www.intellisms.co.uk). +intelliSmsFconnection.send.validator.invalid=Anda tidak dapat mengonfigurasi koneksi tanpa fungsi KIRIM dan TERIMA +intelliSmsFconnection.receive.validator.invalid=Anda tidak bisa mengonfigurasi koneksi tanda fungsi KIRIM atau TERIMA +#Controllers +contact.label=Kontak +contact.edited.by.another.user=Pengguna lain sedang memperbarui Kontak ini ketika Anda tengah menyunting +contact.exists.prompt=Sudah ada kontak dengan nomor ini sebelumnya +contact.exists.warn=Kontak dengan nomor ini sudah ada +contact.view.duplicate=Lihat duplikasi +contact.addtogroup.error=Tidak bisa menambah dan mengeluarkan dari grup yang sama! +contact.mobile.label=Telepon genggam +fconnection.label=Fconnection +fconnection.name=Fconnection +fconnection.unknown.type=Tipe koneksi tidak dikenali: +fconnection.test.message.sent=Tes pesan terkirim! +announcement.saved=Pengumuman sudah disimpan dan pesan sudah dalam antrian untuk dikirim +announcement.not.saved=Pengumuman tidak dapat disimpan! +announcement.save.success={0} Pengumuman telah disimpan! +announcement.id.exist.not=Tidak dapat menemukan pengumuman dengan id {0} +autoreply.save.success={0} Balasan otomatis telah disimpan! +autoreply.not.saved=Balasan otomatis tidak dapat disimpan! +report.creation.error=Ada kesalahan dalam membuat laporan +export.message.title=Ekspor Pesan FrontlineSMS +export.database.id=DatabaseID +export.message.date.created=Tanggal Dibuat +export.message.text=Teks +export.message.destination.name=Nama yang dituju +export.message.destination.mobile=Nomor telepon genggam yang dituju +export.message.source.name=Nama Pengirim +export.message.source.mobile=Telepon genggam Pengirim +export.contact.title=Ekspor Kontak FrontlineSMS +export.contact.name=Nama +export.contact.mobile=Telepon genggam +export.contact.email=Email +export.contact.notes=Catatan +export.contact.groups=Grup +export.messages.name1={0} {1} ({2} pesan) +export.messages.name2={0} ({1} pesan) +export.contacts.name1={0} grup ({1} kontak) +export.contacts.name2={0} grup pintar ({1} kontak) +export.contacts.name3=Semua kontak ({0} kontak) +folder.archived.successfully=Folder berhasil diarsipkan! +folder.unarchived.successfully=Folder berhasil dibuka! +folder.trashed=Folder sudah dibuang ke tempat sampah! +folder.restored=Folder sudah dikembalikan! +folder.exist.not=Tidak dapat menemukan folder dengan id {0} +folder.renamed=Nama Folder Sudah Diubah +group.label=Grup +group.name.label=Nama +group.update.success=Grup berhasil diperbarui +group.save.fail=Gagal menyimpan grup +group.delete.fail=Tidak bisa menghapus grup +import.label=Import contacts and messages +import.backup.label=You can use the form below to import your contacts. You can also import messages that you have from a previous Frontline project. +import.prompt.type=Select the type of data you wish to import before uploading +import.messages=Messages in Frontline's CSV format +import.prompt=Select the file containing your data to initiate the import +import.upload.failed=Gagal mengunggah berkas karena satu dan lain hal +import.contact.save.error=Menemukan kesalahan dalam menyimpan kontak +import.contact.complete={0} kontak sudah diimpor; {1} gagal +import.contact.exist=The imported contacts already exist. +import.contact.failed.label=Failed contact imports +import.contact.failed.info={0} contact(s) successfully imported.
{1} contact(s) could not be imported.
{2} +import.download.failed.contacts=Download a file containing the failed contacts. +import.message.save.error=Menemukan kesalahan dalam menyimpan pesan +import.message.complete={0} pesan sudah diimpor; {1} gagal +export.label=Export data from your Frontline workspace +export.backup.label=You can export your Frontline data as VCF/VCard, CSV or PDF +export.prompt.type=Select which data you wish to export +export.allcontacts=All of your contacts +export.inboxmessages=Your Inbox messages +export.submit.label=Export and download data +many.selected={0} {1} dipilih +flash.message.activity.found.not=Aktivitas tidak dapat ditemukan +flash.message.folder.found.not=Folder tidak dapat ditemukan +flash.message=Pesan +flash.message.fmessage={0} pesan +flash.message.fmessages.many={0} pesan SMS +flash.message.fmessages.many.one=1 pesan SMS +fmessage.exist.not=Tidak dapat menemukan pesan dengan id {0} +flash.message.poll.queued=Poll sudah disimpan dan pesan sudah berada dalam antrian untuk dikirim +flash.message.poll.not.saved=Poll tidak bisa disimpan! +system.notification.ok=OK +system.notification.fail=GAGAL +flash.smartgroup.delete.unable=Tidak dapat menghapus grup pintar +flash.smartgroup.saved=Grup pintar {0} disimpan +flash.smartgroup.save.failed=Gagal menyimpan Grup Pintar. Kesalahan {0} +smartgroup.id.exist.not=Tidak dapat menemukan Grup Pintar dengan id {0} +smartgroup.save.failed=Gagal menyimpan grup pintar {0}dengan parameter {1}{2}kesalahan: {3} +searchdescriptor.searching=Mencari +searchdescriptor.all.messages=semua pesan +searchdescriptor.archived.messages=, termasuk pesan yang diarsipkan +searchdescriptor.exclude.archived.messages=, tanpa pesan yang diarsipkan +searchdescriptor.only=, hanya {0} +searchdescriptor.between=, antara {0} dan {1} +searchdescriptor.from=, dari {0} +searchdescriptor.until=, sampai {0} +poll.title={0} +announcement.title={0} +autoreply.title={0} +folder.title={0} +frontlinesms.welcome=Selamat datang di FrontlineSMS! \\o/ +failed.pending.fmessages={0} pesan tertunda gagal. Pergi ke pesan tertunda untuk melihat. +subscription.title={0} +subscription.info.group=Grup: {0} +subscription.info.groupMemberCount={0} anggota +subscription.info.keyword=Kata kunci terpopuler: {0} +subscription.sorting.disable=Matikan fungsi sortir otomatis +subscription.info.joinKeywords=Bergabung: {} +subscription.info.leaveKeywords=Keluar: {0} +subscription.group.goto=Tampilkan Grup +subscription.group.required.error=Berlangganan harus mempunyai sebuah grup +subscription.save.success={0} Berlangganan telah tersimpan! +language.label=Bahasa +language.prompt=Ganti bahasa FrontlineSMS di halaman muka pengguna +frontlinesms.user.support=Pertolongan Pengguna FrontlineSMS +download.logs.info1=WARNING: Tim FrontlineSMS tidak bisa langsung menjawab log yang didaftarkan. Jika Anda punya permintaan pertolongan bagi pengguna, silakan klik berkas "Pertolongan" untuk melihat apakah Anda bisa menemukan jawabannya di sana. Jika tidak, laporkan permasalahan Anda lewat forum pertolongan pengguna kami: +download.logs.info2=Pengguna lain mungkin sudah melaporkan permasalahan yang sama dan mendapatkan solusinya! Untuk melanjutkan dan memasukkan pertanyaan Anda, silakan klik 'Lanjutkan' +# Configuration location info +configuration.location.title=Konfigurasi Lokasi +configuration.location.description=These files include your database and other settings, which you may wish to back up elsewhere. +configuration.location.instructions=Anda bisa melihat konfigurasi aplikasi anda di {1}. Di dalam berkas ini terdapat basis data dan konfigurasi lainnya, silahkan anda salin untuk cadangan. +dynamicfield.contact_name.label=Nama Kontak +dynamicfield.contact_number.label=Nomor Kontak +dynamicfield.keyword.label=Kata Kunci +dynamicfield.message_content.label=Isi Pesan +# TextMessage domain +fmessage.queued=Pesan sudah dalam antrian untuk dikirim ke {0} +fmessage.queued.multiple=Pesan sudah dalam antrian untuk dikirim ke {0} penerima +fmessage.retry.success=Pesan sudah dalam antrian lagi untuk dikirim ke {0} +fmessage.retry.success.multiple={0} pesan sudah dalam antrian lagi untuk dikirim +fmessage.displayName.label=Nama +fmessage.text.label=Pesan +fmessage.date.label=Tanggal +fmessage.to=To: {0} +fmessage.to.multiple=Kepada: {0} penerima +fmessage.quickmessage=Send message +fmessage.archive=Arsip +fmessage.activity.archive=Arsip {0} +fmessage.unarchive=Buka Arsip {0} +fmessage.export=Ekspor +fmessage.rename=Ubah nama {0} +fmessage.edit=Sunting {0} +fmessage.delete=Delete +fmessage.moreactions=Lainnya... +fmessage.footer.show=Perlihatkan +fmessage.footer.show.failed=Gagal +fmessage.footer.show.all=Semua +fmessage.footer.show.starred=Ditandai +fmessage.footer.show.incoming=Masuk +fmessage.footer.show.outgoing=Keluar +fmessage.archive.back=Kembali +fmessage.activity.sentmessage=({0} pesan terkirim +fmessage.failed=gagal +fmessage.header=pesan +fmessage.section.inbox=Kotak Masuk +fmessage.section.sent=Terkirim +fmessage.section.pending=Tertunda +fmessage.section.trash=Tempat sampah +fmessage.addsender=Tambahkan ke kontak +fmessage.resend=Kirim ulang +fmessage.retry=Coba lagi +fmessage.reply=Balas +fmessage.forward=Forward +fmessage.messages.none=Tidak ada pesan di sini! +fmessage.selected.none=Tidak ada pesan dipilih +fmessage.move.to.header=Pindahkan pesan ke... +fmessage.move.to.inbox=Kotak Masuk +fmessage.archive.many=Archive selected +fmessage.count=1 pesan +fmessage.count.many={0} pesan +fmessage.many=pesan +fmessage.delete.many=Delete selected +fmessage.reply.many=Reply selected +fmessage.restore=Kembalikan +fmessage.restore.many=Kembalikan +fmessage.retry.many=Retry selected +fmessage.selected.many={0} pesan dipilih +fmessage.unarchive.many=Unarchive selected +# TODO move to poll.* +fmessage.showpolldetails=Tunjukkan grafik +fmessage.hidepolldetails=Sembunyikan grafik +# TODO move to search.* +fmessage.search.none=Tidak ada pesan ditemukan. +fmessage.search.description=Mulai pencarian baru di sebelah kiri +fmessage.connection.receivedon=Diterima pada: +activity.name=Nama +activity.delete.prompt=Pindahkan {0} ke tempat sampah. Ini akan memindahkan seluruh pesan yang terkait ke tempat sampah. +activity.label=Aktivitas +activity.categorize=Kategorikan respon +magicwand.title=Tambahkan ekspresi pengganti +folder.create.success=Folder berhasil dibuat +folder.create.failed=Tidak bisa membuat folder +folder.name.validator.error=Nama folder sudah digunakan +folder.name.blank.error=Nama folder tidak bisa dikosongkan +poll.name.blank.error=Nama poll tidak bisa dikosongkan +poll.name.validator.error=Nama poll sudah digunakan +autoreply.name.blank.error=Nama balasan otomatis tidak bisa dikosongkan +autoreply.name.validator.error=Nama balasan otomatis sudah digunakan +announcement.name.blank.error=Judul pengumuman tidak bisa dikosongkan +announcement.name.validator.error=Judul pengumuman sudah dipakai +group.name.blank.error=Nama grup tidak bisa dikosongkan +group.name.validator.error=Nama grup sudah dipakai +#Jquery Validation messages +jquery.validation.required=Data ini wajib diisi. +jquery.validation.remote=Silahkan membenarkan data ini. +jquery.validation.email=Tolong masukkan alamat email yang valid. +jquery.validation.url=Silahkan masukkan URL yang valid. +jquery.validation.date=Silahkan masukkan tanggal yang valid. +jquery.validation.dateISO=Silahkan masukkan tanggal yang valid (ISO). +jquery.validation.number=Silahkan masukkan angka yang valid. +jquery.validation.digits=Harap hanya memasukkan angka. +jquery.validation.creditcard=Silahkan masukkan nomor kartu kredit yang valid. +jquery.validation.equalto=Silahkan masukkan angka yang sama kembali. +jquery.validation.accept=Silahkan masukkan nilai dengan ekstensi yang valid. +jquery.validation.maxlength=Silahkan masukkan tidak lebih dari {0} karakter. +jquery.validation.minlength=Silahkan masukkan sedikitnya {0} karakter. +jquery.validation.rangelength=Silahkan masukkan angka antara {0} dan {1} karakter. +jquery.validation.range=Silahkan masukkan angka antara {0} dan {1}. +jquery.validation.max=Silahkan masukkan angka yang kurang dari atau sama dengan {0}. +jquery.validation.min=Silahkan masukkan angka yang lebih besar atau sama dengan {0}. +# Webconnection common +webconnection.select.type=Pilih Web Servis atau aplikasi untuk koneksi ke: +webconnection.type=Pilih Tipe +webconnection.title={0} +webconnection.label=Koneksi Web +webconnection.description=Koneksi ke web servis +webconnection.sorting=Sortir otomatis +webconnection.configure=Konfigurasi servis +webconnection.api=Tampilkan API +webconnection.api.info=FrontlineSMS bisa dikonfigurasi untuk menerima permintaan dari remote servis dan mengirimkan pesan keluar. Informasi selengkapnya, lihat di bantuan bagian Koneksi Web. +webconnection.api.enable.label=Aktifkan API +webconnection.api.secret.label=PIN API +webconnection.api.disabled=API Dimatikan +webconnection.api.url=URL API +webconnection.moreactions.retryFailed=retry failed uploads +webconnection.failed.retried=Failed web connections have been scheduled for resending. +webconnection.url.error.locahost.invalid.use.ip=Please use 127.0.0.1 instead of "locahost" for localhost urls +webconnection.url.error.url.start.with.http=Invalid URL (should start with http:// or https://) +# Webconnection - generic +webconnection.generic.label=Web servis lain +webconnection.generic.description=Kirim pesan ke web servis lain +webconnection.generic.subtitle=HTTP Koneksi Web +# Webconnection - Ushahidi/Crowdmap +webconnection.ushahidi.label=Crowdmap / Ushahidi +webconnection.ushahidi.description=Kirim pesan ke server Crowdmap atau Ushahidi. +webconnection.ushahidi.key.description=PIN API untuk Crowdmap atau Ushahidi bisa ditemukan di bagian Pengaturan pada situs Crowdmap atau Ushahidi. +webconnection.ushahidi.url.label=Alamat: +webconnection.ushahidi.key.label=PIN API Ushahidi: +webconnection.crowdmap.url.label=Alamat penempatan Crowdmap: +webconnection.crowdmap.key.label=PIN API Crowdmap: +webconnection.ushahidi.serviceType.label=Pilih servis +webconnection.ushahidi.serviceType.crowdmap=Crowdmap +webconnection.ushahidi.serviceType.ushahidi=Ushahidi +webconnection.crowdmap.url.suffix.label=.crowdmap.com +webconnection.ushahidi.subtitle=Koneksi Web ke {0} +webconnection.ushahidi.service.label=Servis: +webconnection.ushahidi.fsmskey.label=PIN API FrontlineSMS: +webconnection.ushahidi.crowdmapkey.label=PIN Crowdmap/Ushahidi: +webconnection.ushahidi.keyword.label=Kata kunci: +url.invalid.url=The URL provided is invalid. +webconnection.confirm=Konfirmasi +webconnection.keyword.title=Pindahkan setiap pesan masuk yang mengandung kata kunci berikut: +webconnection.all.messages=Jangan menggunakan kata kunci (Semua pesan masuk akan diteruskan ulang ke Koneksi web ini) +webconnection.httpMethod.label=Pilih Metode HTTP: +webconnection.httpMethod.get=GET +webconnection.httpMethod.post=POST +webconnection.name.prompt=Namai koneksi web ini +webconnection.details.label=Rincian konfirmasi +webconnection.parameters=Informasi konfigurasi telah dikirimkan ke server +webconnection.parameters.confirm=Informasi yang terkonfigurasi telah dikirimkan ke server +webconnection.keyword.label=Kata kunci: +webconnection.none.label=Tidak ada +webconnection.url.label=Url Server: +webconnection.param.name=Nama: +webconnection.param.value=Nilai: +webconnection.add.anotherparam=Masukkan parameter +dynamicfield.message_body.label=Pesan Teks +dynamicfield.message_body_with_keyword.label=Pesan Teks dengan kata kunci +dynamicfield.message_src_number.label=Nomor kontak +dynamicfield.message_src_name.label=Nama kontak +dynamicfield.message_timestamp.label=Format waktu Pesan +webconnection.keyword.validation.error=Kata kunci wajib diisi +webconnection.url.validation.error=Url wajib diisi +webconnection.save=Koneksi Web berhasil disimpan! +webconnection.saved=Koneksi Web telah disimpan! +webconnection.save.success={0} Koneksi Web berhasil disimpan! +webconnection.generic.service.label=Servis: +webconnection.generic.httpMethod.label=Metode http: +webconnection.generic.url.label=Alamat: +webconnection.generic.parameters.label=Informasi konfigurasi berhasil dikirim ke server: +webconnection.generic.keyword.label=Keyword: +webconnection.generic.key.label=PIN API: +frontlinesms2.Keyword.value.validator.error.frontlinesms2.UshahidiWebconnection.keyword.value=Nilai kata kunci tidak valid +#Subscription i18n +subscription.label=Berlangganan +subscription.name.prompt=Beri nama Berlangganan ini +subscription.details.label=Rincian konfirmasi +subscription.description=Ijinkan pengunjung untuk bergabung dan keluar dari grup secara otomatis dengan mengirimkan pesan memakai kata kunci +subscription.select.group=Pilih grup untuk berlangganan +subscription.group.none.selected=Pilih grup +subscription.autoreplies=Balasan otomatis +subscription.sorting=Sortir otomatis +subscription.sorting.header=Proses pesan secara otomatis menggunakan kata kunci (pilihan) +subscription.confirm=Konfirmasi +subscription.group.header=Pilih Grup +subscription.group.description=Kontak bisa ditambahkan atau dihapus dari grup secara otomatis ketika FrontlineSMS menerima pesan dengan kata kunci tertentu. +subscription.keyword.header=Enter keywords for this subscription +subscription.top.keyword.description=Masukkan kata kunci terpopuler yang akan dipakai pengguna untuk memilih grup ini +subscription.top.keyword.more.description=Anda boleh memasukkan kata kunci terpopuler untuk setiap pilihan, dipisahkan dengan tanda koma. Kata kunci terpopuler ini harus unik untuk semua aktivitas. +subscription.keywords.header=Masukkan kata kunci untuk bergabung atau berhenti dari grup ini. +subscription.keywords.description=Anda boleh memasukkan beberapa kata kunci dipisahkan dengan koma. Jika kata kunci terpopuler dikosongkan, maka kata kunci untuk bergabung dan berhenti harus unik untuk semua aktivitas. +subscription.default.action.header=Pilih aksi apabila tidak ada kata kunci yang terkirim +subscription.default.action.description=Pilih aksi yang ingin dijalankan ketika ada pesan menggunakan kata kunci terpopuler tetapi bukan kata kunci untuk bergabung atau berhenti: +subscription.keywords.leave=Kata kunci untuk berhenti +subscription.keywords.join=Kata kunci untuk bergabung +subscription.default.action.join=Masukkan kontak ke grup +subscription.default.action.leave=Hapus kontak dari grup +subscription.default.action.toggle=Toggle kontak anggota grup +subscription.autoreply.join=Kirim balasan otomatis ketika kontak bergabung dengan grup +subscription.autoreply.leave=Kirim balasan otomatis ketika kontak berhenti dari grup +subscription.confirm.group=Grup +subscription.confirm.keyword=Kata kunci +subscription.confirm.join.alias=Kata kunci untuk bergabung +subscription.confirm.leave.alias=Kata kunci untuk berhenti +subscription.confirm.default.action=Aksi Semula +subscription.confirm.join.autoreply=Balasan otomatis untuk bergabung +subscription.confirm.leave.autoreply=Balasan otomatis untuk berhenti +subscription.info1=Berlangganan sudah disimpan dan aktif +subscription.info2=Pesan masuk yang sesuai dengan kata kunci ini akan mengubah kontak dari anggota grup +subscription.info3=Untuk melihat berlangganan, silahkan klik kiri di menu +subscription.categorise.title=Kategorikan pesan +subscription.categorise.info=Silahkan pilih aksi yang ingin dijalankan untuk pengirim pesan terpilih ketika mereka dimasukkan ke {0} +subscription.categorise.join.label=Masukkan pengirim pesan ke {0} +subscription.categorise.leave.label=Hapus pengirim pesan dari {0} +subscription.categorise.toggle.label=Toggle pengirim' anggota dari {0} +subscription.join=Bergabung +subscription.leave=Berhenti +subscription.sorting.example.toplevel=contoh SOLUSI +subscription.sorting.example.join=contoh BERLANGGANAN, BERGABUNG +subscription.sorting.example.leave=contoh BERHENTI, KELUAR +subscription.keyword.required=Kata kunci tidak boleh kosong +subscription.jointext.required=Silahkan tulis balasan otomatis untuk bergabung +subscription.leavetext.required=Silahkan tulis balasan otomatis untuk berhenti +subscription.moreactions.delete=Hapus berlangganan +subscription.moreactions.rename=Ubah nama berlangganan +subscription.moreactions.edit=Sunting berlangganan +subscription.moreactions.export=Ekspor berlangganan +# Generic activity sorting +activity.generic.sorting=Pemrosesan otomatis +activity.generic.sorting.subtitle=Memproses pesan otomatis menggunakan kata kunci (pilihan) +activity.generic.sort.header=Memproses pesan otomatis menggunakan kata kunci (pilihan) +activity.generic.sort.description=Jika pengunjung mengirimkan pesan yang diawali dengan kata kunci tertentu, FrontlineSMS dapat secara otomatis memproses pesan tersebut di dalam sistem anda. +activity.generic.keywords.title=Silahkan masukkan kata kunci untuk aktivitas. Anda boleh menggunakan beberapa kata kunci dipisahkan dengan tanda koma: +activity.generic.keywords.subtitle=Silahkan masukkan kata kunci untuk aktivitas +activity.generic.keywords.info=Silahkan masukkan beberapa kata kunci dipisahkan dengan tanda koma: +activity.generic.no.keywords.title=Dilarang menggunakan kata kunci +activity.generic.no.keywords.description=Semua pesan masuk yang tidak sesuai dengan kata kunci yang ada akan memengaruhi aktivitas ini +activity.generic.disable.sorting=Jangan mensortir pesan secara otomatis +activity.generic.disable.sorting.description=Pesan tidak akan diproses secara otomatis oleh aktivitas ini +activity.generic.enable.sorting=Proses balasan yang mengandung kata kunci secara otomatis +activity.generic.sort.validation.unique.error=Kata kunci harus unik +activity.generic.keyword.in.use=Kata kunci {0} sudah digunakan untuk aktivitas {1} +activity.generic.global.keyword.in.use=Aktivitas {0} diatur untuk menerima semua pesan yang tidak sesuai dengan kata kunci. Anda hanya diperbolehkan mempunyai satu aktivitas yang aktif dengan aturan ini. +#basic authentication +auth.basic.label=Basic Authentication +auth.basic.info=Require a username and password for accessing FrontlineSMS across the network +auth.basic.enabled.label=Enable Basic Authentication +auth.basic.username.label=Nama Pengguna +auth.basic.password.label=Kata Sandi +auth.basic.confirmPassword.label=Confirm Password +auth.basic.password.mismatch=Passwords don't match +newfeatures.popup.title=Fitur baru +newfeatures.popup.showinfuture=Tampilkan kembali pesan ini +dynamicfield.message_text.label=Pesan teks +dynamicfield.message_text_with_keyword.label=Pesan Teks dengan Kata kunci +dynamicfield.sender_name.label=Nama pengirim +dynamicfield.sender_number.label=Nomor pengirim +dynamicfield.recipient_number.label=Nomor penerima +dynamicfield.recipient_name.label=Nama penerima +# Smpp Fconnection +smpp.label=SMPP Account +smpp.type.label=Tipe +smpp.name.label=Nama +smpp.send.label=Use for sending +smpp.receive.label=Use for receiving +smpp.url.label=SMSC URL +smpp.port.label=SMSC Port +smpp.username.label=Nama Pengguna +smpp.password.label=Kata Sandi +smpp.fromNumber.label=Dari Nomor +smpp.description=Send and receive messages through an SMSC +smpp.global.info=You will need to get an account with your phone network of choice. +smpp.send.validator.invalid=You cannot configure a connection without SEND or RECEIVE fuctionality. +routing.title=Create rules for which phone number is used by outgoing messages. +routing.info=These rules will determine how the system selects which connection or phone number to use to send outgoing messages. Remember, the phone number seen by recipients may depend on the rules you set here. Also, changing this configuration may affect the cost of sending messages. +routing.rules.sending=When sending outgoing messages: +routing.rules.not_selected=If none of the above rules match: +routing.rules.otherwise=Otherwise: +routing.rules.device=Use {0} +routing.rule.uselastreceiver=Send through most recent number that the contact messaged +routing.rule.useany=Use any available connection's phone number +routing.rule.dontsend=Do not send the message +routing.notification.no-available-route=Outgoing message(s) not sent due to your routing preferences. +routing.rules.none-selected.warning=Warning: You have no rules or phone numbers selected. No messages will be sent. If you wish to send messages, please enable a connection. +customactivity.overview=Overview +customactivity.title={0} +customactivity.confirm=Konfirmasi +customactivity.label=Custom Activity Builder +customactivity.description=Create your own activity from scratch by applying a custom set of actions to your specified keyword +customactivity.name.prompt=Name this activity +customactivity.moreactions.delete=Delete activity +customactivity.moreactions.rename=Rename activity +customactivity.moreactions.edit=Edit activity +customactivity.moreactions.export=Export activity +customactivity.text.none=Tidak ada +customactivity.config=Configure +customactivity.config.description=Build and configure a set of actions for this activity. The actions will all be executed when a message matches the criteria you set on the previous step. +customactivity.info=Your Custom Activity has been created, and any messages containing your keyword will have the specified actions applied to it. +customactivity.info.warning=Without a keyword, all incoming messages will trigger the actions in this Custom Activity. +customactivity.info.note=Note: If you archive the Custom Activity, incoming messages will no longer be sorted for it. +customactivity.save.success={0} activity saved +customactivity.action.steps.label=Action Steps +validation.group.notnull=Please select a group +customactivity.join.description=Joining "{0}" group +customactivity.leave.description=Leaving "{0}" group +customactivity.forward.description=Forwarding with "{0}" +customactivity.webconnectionStep.description=Upload to "{0}" +customactivity.reply.description=Reply with "{0}" +customactivity.step.join.add=Add sender to group +customactivity.step.join.title=Add sender to group* +customactivity.step.leave.add=Hapus pengirim pesan dari {0} +customactivity.step.leave.title=Hapus pengirim pesan dari {0} +customactivity.step.reply.add=Send Autoreply +customactivity.step.reply.title=Enter message to autoreply to sender* +customactivity.step.forward.add=Forward message +customactivity.step.forward.title=Automatically forward a message to one or more contacts +customactivity.manual.sorting=Automatic processing disabled +customactivity.step.webconnectionStep.add=Upload message to a URL +customactivity.step.webconnectionStep.title=Upload message to a URL +customactivity.validation.error.autoreplytext=Reply message is required +customactivity.validation.error.name=Url wajib diisi +customactivity.validation.error.url=Url wajib diisi +customactivity.validation.error.paramname=Parameter name is required +recipientSelector.keepTyping=Keep typing... +recipientSelector.searching=Mencari +validation.recipients.notnull=Please select at least one recipient +localhost.ip.placeholder=your-ip-address diff --git a/plugins/frontlinesms-core/grails-app/i18n/messages_jp.properties b/plugins/frontlinesms-core/grails-app/i18n/messages_jp.properties new file mode 100644 index 000000000..a422fd8ad --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/i18n/messages_jp.properties @@ -0,0 +1,629 @@ +# FrontlineSMS English translation by the FrontlineSMS team, Nairobi +language.name=Japanese +# General info +app.version.label=Versie +# Common action imperatives - to be used for button labels and similar +action.ok=OK +action.close=Sluiten +action.cancel=Annuleren +action.done=Uitgevoerd +action.next=Volgende +action.prev=Vorige +action.back=Terug +action.create=Aanmaken +action.edit=Bewerken +action.rename=Nieuwe naam geven +action.save=Opslaan +action.save.all=Save Selected +action.delete=Verwijderen +action.delete.all=Delete Selected +action.send=Verzenden +action.export=Exporteren +# Messages when FrontlineSMS server connection is lost +server.connection.fail.title=De verbinding met de server is mislukt. +server.connection.fail.info=FrontlineSMS opnieuw opstarten, of dit venster sluiten. +#Connections: +connection.creation.failed=De verbinding kon niet tot stand worden gebracht {0} +connection.route.disabled={0} から {1} への経路が破棄されました +connection.route.successNotification=Route met succes tot stand gebracht op {0} +connection.route.failNotification=Failed to create connection on {1}: {2} [[[edit]](({0}))]. +connection.route.disableNotification={0} への経路を切断 +connection.test.sent=Testbericht met succes verzonden naar{0} via{1} +# Connection exception messages +connection.error.org.smslib.alreadyconnectedexception=Apparaat is al verbonden +connection.error.org.smslib.gsmnetworkregistrationexception=Registratie bij GSM network is mislukt +connection.error.org.smslib.invalidpinexception=Ingevoerde PIN incorrect +connection.error.org.smslib.nopinexception=PIN vereist maar niet ingevoerd +connection.error.java.io.ioexception=Port heeft een fout veroorzaakt: {0} +connection.header=Settings > Connections +connection.list.none=U heeft geen verbindingen geconfigureerd. +connection.edit=Verbinding bewerken +connection.delete=Verbinding verwijderen +connection.deleted=Verbinding {0} is verwijderd. +connection.add=Nieuwe verbinding toevoegen +connection.createtest.message.label=Testbericht +connection.route.disable=経路を破棄 +connection.send.test.message=Test SMS verzenden +connection.test.message=Gefeliciteerd namens FrontlineSMS \\o/ U heeft {0} met succes samengesteld en U kunt nu een SMS verzenden\\o/ +connection.validation.prompt=Alle verplichte velden invullen a.u.b. +connection.select=Verbindingstype kiezen +connection.type=Type kiezen +connection.details=Gegevens invoeren +connection.confirm=Bevestigen +connection.createtest.number=Nummer +connection.confirm.header=Instellingen bevestigen +connection.name.autoconfigured=Automatisch geconfigureerd {0} {1} op port {2}" +status.connection.none=U heeft geen verbindingen geconfigureerd +status.devises.header=Apparatuur bespeurd +status.detect.modems=Modems bespeuren +status.modems.none=Nog geen apparatuur bespeurd. +connectionstatus.not_connected=Niet verbonden +connectionstatus.connecting=Verbinding wordt tot stand gebracht +connectionstatus.connected=Verbonden +default.doesnt.match.message=Eigenschap [{0}] van klasse [{1}] met waarde [{2}] komt niet overeen met het vereiste pattern [{3}] +default.invalid.url.message=Eigenschap [{0}] van klasse [{1}] met waarde [{2}] is geen geldige URL +default.invalid.creditCard.message=Eigenschap [{0}] van klasse [{1}] met waarde [{2}] is geen geldig credit card nummer +default.invalid.email.message=Eigenschap [{0}] van klasse [{1}] met waarde [{2}] is is geen geldig e-mailadres +default.invalid.range.message=Eigenschap [{0}] van klasse [{1}] v [{2}] valt niet in de geldige waardenreeks van [{3}] tot [{4}] +default.invalid.size.message=Eigenschap [{0}] van klasse [{1}] v [{2}] valt niet in de geldige grootte van [{3}] tot [{4}] +default.invalid.max.message=Eigenschap [{0}] van klasse [{1}] met waarde [{2}] overschrijdt de maximumwaarde [{3}] +default.invalid.min.message=Eigenschap [{0}] van klasse [{1}] met waarde [{2}] is minder dan de minimumwaarde [{3}] +default.invalid.max.size.message=Eigenschap [{0}] van klasse [{1}] v [{2}] overschrijdt de maximumgrootte van [{3}] +default.invalid.min.size.message=Eigenschap [{0}] van klasse [{1}] v [{2}] is minder dan minimumgrootte van [{3}] +default.invalid.validator.message=Eigenschap [{0}] van klasse [{1}] v [{2}] is niet geldig +default.not.inlist.message=Eigenschap [{0}] van klasse [{1}] met waarde [{2}] komt niet voor in de lijst [{3}] +default.blank.message=Eigenschap [{0}] van klasse [{1}] mag niet leeg zijn +default.not.equal.message=Eigenschap [{0}] van klasse [{1}] met waarde [{2}] cannot equal [{3}] +default.null.message=Eigenschap [{0}] van klasse [{1}] mag niet leeg zijn +default.not.unique.message=Eigenschap [{0}] van klasse [{1}] met waarde [{2}] moet uniek zijn +default.paginate.prev=Vorige +default.paginate.next=Volgende +default.boolean.true=Juist +default.boolean.false=Onjuist +default.date.format=yyyy/MM/dd hh:mm +default.number.format=0 +default.unarchived={0} gede-archiveerd +default.unarchive.failed=De-archiveren mislukt {0} +default.trashed={0} moved to trash +default.restored={0} teruggezet +default.restore.failed=kon niet worden teruggezet {0} met id {1} +default.archived={0} archived +default.archived.multiple={0} gearchiveerd +default.created={0} Aangemaakt +default.created.message={0} {1} is aangemaakt +default.create.failed=Aanmaken is mislukt {0} +default.updated={0} is bijgewerkt +default.update.failed=Bijwerking mislukt {0} met id {1} +default.updated.multiple={0} is bijgewerkt +default.updated.message={0} bijgewerkt +default.deleted={0} gewist +default.trashed.multiple={0} verwijderd naar de prullenmand +default.unarchive.keyword.failed=De-archiveren {0} mislukt. Trefwoord of naam in gebruik +default.unarchived.multiple={0} gede-archiveerd +default.delete.failed=Kon niet worden verwijderd {0} met id {1} +default.notfound=Niet gevonden {0} met id {1} +default.optimistic.locking.failure=Een andere gebruiker heeft dit bijgewerkt {0} tijdens Uw bewerking +default.home.label=ホーム +default.list.label={0} Lijst +default.add.label=Toevoegen {0} +default.new.label=Nieuw {0} +default.create.label=Aanmaken {0} +default.show.label=Zichtbaar maken {0} +default.edit.label=Bewerken {0} +search.clear=Zoekactie annuleren +default.button.create.label=Aanmaken +default.button.edit.label=Bewerken +default.button.update.label=Bijwerken +default.button.delete.label=Verwijderen +default.button.search.label=Zoeken +default.button.apply.label=実行 +default.button.delete.confirm.message=Bent U zeker? +default.deleted.message={0} verwijderd +# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) +typeMismatch.java.net.URL=Eigenschap {0} moet een geldige URL zijn +typeMismatch.java.net.URI=Eigenschap {0} moet een geldige URL zijn +typeMismatch.java.util.Date=Eigenschap {0} oet een geldige datum zijn +typeMismatch.java.lang.Double=Eigenschap {0} moet een geldig nummer zijn +typeMismatch.java.lang.Integer=Eigenschap {0} moet een geldig nummer zijn +typeMismatch.java.lang.Long=Eigenschap {0} moet een geldig nummer zijn +typeMismatch.java.lang.Short=Eigenschap {0} moet een geldig nummer zijn +typeMismatch.java.math.BigDecimal=Eigenschap {0} moet een geldig nummer zijn +typeMismatch.java.math.BigInteger=Eigenschap {0} v +typeMismatch.int={0} moet een geldig nummer zijn +# Application specific messages +messages.trash.confirmation=Dit zal de prullenbak legen en de berichten definitief verwijderen. Wilt u doorgaan? +default.created.poll=De poll is aangemaakt! +default.search.label=Zoekfunctie annuleren +default.search.betweendates.title=Tussen datums: +default.search.moresearchoption.label=Verdere zoekopties +default.search.date.format=yyyy/MM/dd +default.search.moreoption.label=Verdere opties +# Messages Tab +message.create.prompt=bericht invoeren +message.character.count=resterende tekens {0} ({1} SMS message(s)) +message.character.count.warning=Kan langer zijn na ingevoerde vervangingen +announcement.label=Aankondiging +announcement.description=Verstuur een aankondigingsbericht en organiseer de antwoorden +announcement.info1=De aankondiging is opgeslagen en de berichten zijn toegevoegd aan de wachtrij +announcement.info2=Het kan enige tijd duren voordat alle berichten verzonden zijn,afhankelijk van het aantal berichten en de netwerk verbinding. +announcement.info3=Om het verloop van de berichten te zien, open de'In behandeling' berichten map +announcement.info4=Om de aankondiging te zien, klik op het in het menu aan de linkerkant. +announcement.validation.prompt=Alle verplichte velden invullen a.u.b. +announcement.select.recipients=Selecteer de ontvangers +announcement.confirm=Bevestigen +announcement.delete.warn=verwijderen {0} WAARSCHUWING: Dit kan niet verwijderd worden ! +announcement.prompt=Geef deze aankondiging een naam +announcement.confirm.message=bericht +announcement.details.label=Gegevens bevestigen +announcement.message.label=bericht +announcement.message.none=geen +announcement.recipients.label=Ontvangers +announcement.create.message=bericht aanmaken +#TODO embed javascript values +announcement.recipients.count=geselecteerde contactnamen +announcement.messages.count=berichten worden verzonden +announcement.moreactions.delete=Aankondiging verwijderen +announcement.moreactions.rename=Aankondiging een nieuwe naam geven +announcement.moreactions.edit=Aankondiging bewerken +announcement.moreactions.export=Aankondiging exporteren +frontlinesms2.Announcement.name.unique.error.frontlinesms2.Announcement.name={1} {0} "{2}" moet uniek zijn +archive.inbox=Postvak IN archief +archive.sent=Verstuurd archief +archive.activity=Activiteit archief +archive.folder=Folder archief +archive.folder.name=Naam +archive.folder.date=Datum +archive.folder.messages=berichten +archive.folder.none=  Geen gearchiveerde folders +archive.activity.name=Naam +archive.activity.type=タイプ +archive.activity.date=Datum +archive.activity.messages=berichten +archive.activity.list.none=  Geen gearchiveerde activititeiten +autoreply.enter.keyword=Voer het trefwoord in +autoreply.create.message=Voer het automatische antwoord in +autoreply.confirm=Bevestigen +autoreply.name.label=bericht +autoreply.details.label=De gegevens bevestigen +autoreply.label=automatisch antwoord +autoreply.keyword.label=trefwoord(en) +autoreply.description=Automatisch beantwoorden van inkomende berichten +autoreply.info=Het automatische antwoord is gemaakt, berichten die beginnen met Uw trefwoord worden automatisch beantwoordt met deze bericht en op een lijst geplaatst die U kunt bezichtigen door erop te klikken in het menu aan de rechterkant. +autoreply.info.warning=Automatische antwoorden zonder trefwoord gaan naar alle andere inkomende berichten. +autoreply.info.note=Note: Als u het automatische antwoord archiveert dan worden de inkomende berichten niet langer gesorteerd. +autoreply.validation.prompt=Alle verplichte velden invullen a.u.b. +autoreply.message.title=Bericht te verzenden voor dit automatische antwoord: +autoreply.keyword.title=Berichten automatisch sorteren met een trefwoord : +autoreply.name.prompt=Geef dit automatisch antwoord een naam +autoreply.message.count=0 tekens (1 SMS bericht) +autoreply.moreactions.delete=Verwijder het automatische antwoord +autoreply.moreactions.rename=Wijzig de naam van het automatische antwoord +autoreply.moreactions.edit=Bewerk het automatische antwoord +autoreply.moreactions.export=Exporteer het automatische antwoord +autoreply.all.messages=Gebruik het trefwoord niet (Alle inkomende boodscappen ontvangen hetzelfde automatische antwoord) +autoreply.text.none=Geen +frontlinesms2.Autoreply.name.unique.error.frontlinesms2.Autoreply.name={1} {0} "{2}" moet uniek zijn +frontlinesms2.Autoreply.name.validator.error.frontlinesms2.Autoreply.name=Naam van het automatische antwoord moet uniek zijn +frontlinesms2.Keyword.value.validator.error.frontlinesms2.Autoreply.keyword.value=Trefwoord "{2}" is reeds in gebruik +frontlinesms2.Autoreply.autoreplyText.nullable.error.frontlinesms2.Autoreply.autoreplyText=bericht kan niet leeg zijn +contact.new=新規連絡先 +contact.list.no.contact=Geen contacten hier! +contact.header=Contacten +contact.all.contacts=All contacten ({0}) +contact.create=Nieuw contact maken +contact.groups.header=Groepen +contact.create.group=Nieuwe groep maken +contact.smartgroup.header=Smart Groups +contact.create.smartgroup=Nieuwe Smart Group maken +contact.add.to.group=Toevoegen aan de groep... +contact.remove.from.group=Verwijderen uit de groep +contact.customfield.addmoreinformation=Informatie toevoegen... +contact.customfield.option.createnew=Nieuwe maken... +contact.name.label=Naam +contact.phonenumber.label=Telefoonnummer +contact.notes.label=Notities +contact.email.label=電子メール +contact.groups.label=Groepen +contact.notinanygroup.label=Niet gevonden in groepen +contact.messages.label=Berichten +contact.messages.sent={0} 通送信済み +contact.received.messages={0} ontvangen berichten +contact.search.messages=Zoek berichten +group.rename=Groep nieuwe naam geven +group.edit=Groep bewerken +group.delete=Groep verwijderen +group.moreactions=Andere handelingen... +customfield.validation.prompt=Een naam invoeren a.u.b. +customfield.name.label=Naam +export.contact.info=Om contacten te exporteren van FrontlineSMS, kies welk soort export en welke informatie die moet worden opgenomen in de geexporteerde data. +export.message.info=Om berichten te exporteren van FrontlineSMS, kies welk soort export en de informatie die moet worden opgenomen in de geexporteerde data. +export.selectformat=Selecteer een uitvoerindeling +export.csv=CSV indeling te gebruiken in het spreadsheet +export.pdf=PDF indeling om af te drukken +folder.name.label=Naam +group.delete.prompt=Bent U zeker dat U {0} wilt verwijderen? WAARSCHUWING: Dit kan niet ongedaan gemaakt worden +layout.settings.header=Instellingen +activities.header=Activiteiten +activities.create=Een nieuwe activiteit aanmaken +folder.header=Mappen +folder.create=Een nieuw map maken +folder.label=Map +message.folder.header={0} Map +fmessage.trash.actions=Prullenbak acties... +fmessage.trash.empty=Prullenbak leegmaken +fmessage.to.label=Aan +trash.empty.prompt=Alle berichten en activiteiten in the prullenbak zullen permanent verwijderd worden +fmessage.responses.total={0} antwoorden in totaal +fmessage.label=Bericht +fmessage.label.multiple={0} berichten +poll.prompt=Geef deze poll een naam +poll.details.label=Bevestig de gegevens +poll.message.label=Bericht +poll.choice.validation.error.deleting.response=Een opgeslagen keus mag geen lege waarde hebben +poll.alias=Aliassen +poll.aliases.prompt=Voer de aliassen in voor de desbetreffende opties. +poll.sort.label=Autosorteren +poll.autosort.no.description=De antwoorden niet automatisch sorteren. +poll.autosort.description=De antwoorden automatisch sorteren. +poll.sort.keyword=trefwoord +poll.sort.by=Sorteren naar +poll.autoreply.label=automatisch antwoord +poll.autoreply.none=geen +poll.recipients.label=Ontvangers +poll.recipients.none=Geen +#TODO embed javascript values +poll.recipients.count=geselecteerde contacten +poll.messages.count=berichten zullen worden verzonden +poll.yes=Ja +poll.no=Nee +poll.label=アンケート +poll.description=Stuur een vraag en analyseer de antwoorden +poll.messages.sent={0} berichten zijn verzonden +poll.response.enabled=Automatisch antwoord ingeschakeld +poll.message.edit=Bewerk de bericht die verzonden wordt naar de ontvangers +poll.message.prompt=De volgende bericht wordt verzonden naar de ontvangers van de poll +poll.message.count=Resterende tekens 160 (1 SMS message) +poll.moreactions.delete=Poll verwijderen +poll.moreactions.rename=Poll een nieuwe naam geven +poll.moreactions.edit=Poll bewerken +poll.moreactions.export=Poll exporteren +#TODO embed javascript values +poll.reply.text=Reply "{0}" voor Ja, "{1}" voor Nee. +poll.reply.text1={0} "{1}" voor {2} +poll.reply.text2='Ja' of 'Nee'antwoorden a.u.b. +poll.reply.text3=of +poll.reply.text5=Antwoord +poll.reply.text6=Beantwoorden a.u.b. +poll.message.send={0} {1} +poll.recipients.validation.error=Selecteer contacten om berichten naar te verzenden +frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name={1} {0} "{2}" moet uniek zijn +frontlinesms2.Poll.responses.validator.error.frontlinesms2.Poll.responses=De opties voor beantwoording mogen niet identiek zijn +frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value=Trefwoord"{2}" is reeds in gebruik +wizard.title.new=Nieuw +wizard.fmessage.edit.title=Bewerken {0} +popup.title.saved={0} opgeslagen! +popup.activity.create=Create Nieuwe Activiteit : Selecteer type +popup.smartgroup.create=Maak een Smart Group +popup.help.title=ヘルプ +smallpopup.customfield.create.title=Maak een Aangepast Veld +smallpopup.group.rename.title=Geef de groep een nieuwe naam +smallpopup.group.edit.title=De groep bewerken +smallpopup.group.delete.title=De groep verwijderen +smallpopup.fmessage.rename.title=Een nieuwe naam geven {0} +smallpopup.fmessage.delete.title=Verwijderen {0} +smallpopup.fmessage.export.title=Exporteren +smallpopup.delete.prompt=Verwijderen {0} ? +smallpopup.delete.many.prompt=Contacten {0} verwijderen? +smallpopup.empty.trash.prompt=Prullenbak leegmaken? +smallpopup.messages.export.title=Exporteer Results ({0} berichten +smallpopup.test.message.title=Test bericht +smallpopup.recipients.title=Ontvangers +smallpopup.folder.title=Map +smallpopup.group.create.title=Group +smallpopup.contact.export.title=Exporteren +smallpopup.contact.delete.title=Verwijderen +contact.selected.many={0} contacten geselecteerd +group.join.reply.message=Welkom +group.leave.reply.message=さようなら +fmessage.new.info=U heeft {0} nieuwe berichten. Klik om weer te geven +wizard.quickmessage.title=Send Message +wizard.messages.replyall.title=Allemaal beantwoorden +wizard.send.message.title=メッセージを送る +wizard.ok=はい +wizard.create=Maken +wizard.send=Verzenden +common.settings=Instellingen +common.help=ヘルプ +activity.validation.prompt=Alle verplichte velden invullen a.u.b. +autoreply.blank.keyword=Leeg trefwoord. Alle inkomende berichten krijgen een antwoord. +poll.type.prompt=Selecteer welk soort poll aan te maken +poll.question.yes.no=Vraag met een 'Ja' of 'Nee' antwoord +poll.question.multiple=Meerkeuzevraag (bv. 'Rood', 'Blauw', 'Groen') +poll.question.prompt=Vraag invoeren +poll.message.none=Geen vraag verzenden voor deze poll (alleen antwoorden verzamelen). +poll.replies.header=automatisch antwoord van poll antwoorden (optioneel) +poll.replies.description=Als een inkomend bericht herkend wordt als een poll antwoord, een bericht sturen naar de persoon die het antwoord ingezonden heeft. +poll.autoreply.send=Poll antwoorden met een automatisch antwoord beantwoorden +poll.responses.prompt=Mogelijke antwoorden invoeren (tussen 2 en 5) +poll.sort.header=Antwoorden utomatisch sorteren met behulp van een trefwoord (optioneel) +poll.sort.description=Als gebruikders antwoorden sturen met een specifiek trefwoord dan kan FrontlineSMS de berichten automatisch sorteren in uw systeem. +poll.no.automatic.sort=De berichten niet automatissch sorteren +poll.sort.automatically=De berichten met het volgende trefwoord automatisch sorteren +poll.validation.prompt=Alle verplichte velden invullen a.u.b. +poll.name.validator.error.name=Poll benamingen moeten uniek zijn +pollResponse.value.blank.value=Poll antwoord waarde mag niet leeg zijn +poll.question=Vraag invoeren +poll.response=Lijst met antwoorden +poll.sort=Automatisch sorteren +poll.reply=automatisch antwoord +poll.edit.message=Bericht bewerken +poll.recipients=Selecteer ontvangers +poll.confirm=Bevestigen +poll.save=De poll is opgeslagen! +poll.messages.queue=Als u verkozen heeft een bericht met deze poll te verzenden dan zijn de berichten toegevoegd aan de berichten wachtrij. +poll.messages.queue.status=Het kan enige tijd duren voordat alle berichten verzonden zijn, afhankelijk van het aantal berichten en de netwerk verbinding. +poll.pending.messages=Om de status van uw bericht te zien, open de map met de 'In behandeling' berichten +poll.send.messages.none=Geen berichten worden verzonden +quickmessage.details.label=Gegevens bevestigen +quickmessage.message.label=Bericht +quickmessage.message.none=Geen +quickmessage.recipient.label=Ontvanger +quickmessage.recipients.label=Ontvangers +quickmessage.message.count=Resterende ekens 160 (1 SMS bericht +quickmessage.enter.message=Bericht invoeren +quickmessage.select.recipients=Selecteer ontvangers +quickmessage.confirm=Bevestigen +#TODO embed javascript values +quickmessage.recipients.count=geselecteerde contacten +quickmessage.messages.count=berichten worden verzonden +quickmessage.count.label=Aantal berichten: +quickmessage.messages.label=Bericht invoeren +quickmessage.phonenumber.label=Telefoonnummer toevoegen: +quickmessage.phonenumber.add=Toevoegen +quickmessage.selected.recipients=geselecteerde ontvangers +quickmessage.validation.prompt=Alle verplichte velden invullen a.u.b. +fmessage.number.error=Non-numeric characters in this field will be removed when saved +search.filter.label=Zoeken beperken tot +search.filter.group=Selecteer groep +search.filter.activities=Selecteer activiteit/map +search.filter.messages.all=Allen verzonden en ontvangen +search.filter.inbox=Alleen ontvangen berichten +search.filter.sent=Alleen verzonden berichten +search.filter.archive=Archief inbegrepen +search.betweendates.label=Tussen datums +search.header=Zoeken +search.quickmessage=Send message +search.export=Exporteer de resultaten +search.keyword.label=Trefwoord +search.contact.name.label=Contact naam +search.contact.name=Contact naam +search.result.header=Resultaten +search.moreoptions.label=Meer opties +settings.general=Algemeen +settings.connections=Telefoons & verbindingen +settings.logs=Systeem +settings.general.header=Instellingen > Algemeen +settings.logs.header=Settings > System Logs +logs.none=U heeft geen logboeken. +logs.content=Bericht +logs.date=Tijd +logs.filter.label=Toon logboeken voor +logs.filter.anytime=alle tijden +logs.download.label=Download systeem logboeken +logs.download.buttontext=Download Logboeken +logs.download.title=Download logboeken om te verzenden +logs.download.continue=Doorgaan +smartgroup.validation.prompt=Alle verplichte velden invullen a.u.b. Een enkele regel per veld opgeven. +smartgroup.info=Om een Smart group te maken moet u de filtercriteria opgeven die van toepassing zijn voor deze groep +smartgroup.contains.label=houdt in +smartgroup.startswith.label=begint met +smartgroup.add.anotherrule=Voeg nog een regel toe +smartgroup.name.label=Naam +modem.port=ポート +modem.description=Beschrijving +modem.locked=Vergrendeld? +traffic.header=Verkeer +traffic.update.chart=Grafiek bijwerken +traffic.filter.2weeks=Toont de laatste twee weken +traffic.filter.between.dates=Tussen datums +traffic.filter.reset=Filters opnieuw instellen +traffic.allgroups=Alle groepen tonen +traffic.all.folders.activities=Alle activiteiten/mappen tonen +traffic.sent=Verzonden +traffic.received=Ontvangen +traffic.total=Totaal +tab.message=Berichten +tab.archive=Archief +tab.contact=Contacten +tab.status=状態 +tab.search=Zoeken +help.info=Deze versie is een beta dus er is geen ingebouwde Help. Voor hulp in dit stadium raadpleeg de gebruikersforums. +#Controllers +contact.label=Contact(en) +contact.edited.by.another.user=Een andere gebruiker heeft dit Contact bijgewerkt terwijl u bezig was met bijwerken +contact.exists.prompt=Er bestaat al een contact met dit nummer +contact.exists.warn=Een contact met dit nummer bestaat al +contact.view.duplicate=Duplicaat tonen +contact.addtogroup.error=Toevoegen en verwijderen van dezelfde groep is niet mogelijk! +contact.mobile.label=Mobiel +fconnection.label=Fverbinding +fconnection.name=Fverbinding +fconnection.unknown.type=Onbekend type verbinding: +fconnection.test.message.sent=Testbericht in de wachtrij voor verzending! +announcement.saved=De aankondiging is opgeslagen and de boodschap(pen) zijn in de wachtrij voor verzending geplaatst +announcement.not.saved=De aankondiging kon niet opgeslagen worden! +announcement.id.exist.not=Kon de aankondiging niet vinden met id {0} +autoreply.not.saved=automatisch antwoord kon niet worden opgeslagen! +report.creation.error=Rapport is mislukt +export.message.title=FrontlineSMS Bericht Exporteren +export.database.id=データベースID +export.message.date.created=Datum wanneer gemaakt +export.message.text=Tekst +export.message.destination.name=Bestemming Naam +export.message.destination.mobile=Bestemming Mobiel +export.message.source.name=Bron Naam +export.message.source.mobile=Bron Mobiel +export.contact.title=FrontlineSMS Contact Exporteren +export.contact.name=Naam +export.contact.mobile=Mobiel +export.contact.email=Eメール +export.contact.notes=Notities +export.contact.groups=Groepen +export.messages.name1={0} {1} ({2} berichten) +export.messages.name2={0} ({1} berichten) +export.contacts.name1={0} groep ({1} contacten) +export.contacts.name2={0} Smart Group ({1} contacten) +export.contacts.name3=Alle contacten ({0} contacten) +folder.archived.successfully=Map is succesvol gearchiveerd! +folder.unarchived.successfully=Map is gede-archiveerd met succes! +folder.trashed=Map is in de prullenbak! +folder.restored=Map is teruggezet! +folder.exist.not=Kon de map niet vinden met id {0} +folder.renamed=Map heeft een nieuwe naam gekregen +group.label=Groep +group.name.label=Naam +group.update.success=Groep is succesvol bijgewerkt +group.save.fail=Groep opslaan is mislukt +group.delete.fail=Groep kon niet verwijderd worden. In gebruik door een abonnement +import.label=Import contacts and messages +import.backup.label=You can use the form below to import your contacts. You can also import messages that you have from a previous Frontline project. +import.prompt.type=Select the type of data you wish to import before uploading +import.messages=Messages in Frontline's CSV format +import.prompt=Select the file containing your data to initiate the import +import.upload.failed=Uploaden bestand niet gelukt door onbekende reden +import.contact.save.error=Er is een fout opgetreden in het opslaan van het contact +import.contact.complete={0} contacten zijn geimporteerd; {1} mislukt +import.message.save.error=Er is een fout opgetreden in het opslaan van het bericht +import.message.complete={0} berichten zijn geimporteerd; {1} mislukt +many.selected={0} {1}s geselecteerd +flash.message.activity.found.not=Activiteit niet gevonden +flash.message.folder.found.not=Map niet gevonden +flash.message=Bericht +flash.message.fmessage={0} bericht(en) +flash.message.fmessages.many={0} SMS berichten +flash.message.fmessages.many.one=1 SMS bericht +fmessage.exist.not=Kon geen bericht vinden met id {0} +flash.message.poll.queued=Poll is opgeslagen and bericht(en) zijn in de verzend wachtrij geplaatst +flash.message.poll.not.saved=Poll kon niet worden opgeslagen! +system.notification.ok=OK +system.notification.fail=MISLUKT +flash.smartgroup.delete.unable=Smart Group kon niet verwijderd worden +flash.smartgroup.saved=Smart group {0} opgeslagen +flash.smartgroup.save.failed=Smart Group opslaan mislukt. Fouten zijn {0} +smartgroup.id.exist.not=Smart Group met id niet gevonden {0} +smartgroup.save.failed=Opslaan mislukt van Smart Group{0}met params {1}{2}fouten: {3} +searchdescriptor.searching=Bezig met zoeken +searchdescriptor.all.messages=alle berichten +searchdescriptor.archived.messages=, inclusief gearchiveerde berichten +searchdescriptor.exclude.archived.messages=, zonder gearchiveerde berichten +searchdescriptor.only=, uitsluitend {0} +searchdescriptor.between=, tussen {0} en{1} +searchdescriptor.from=, van{0} +searchdescriptor.until=, tot{0} +poll.title={0} +announcement.title={0} +autoreply.title={0} +folder.title={0} +frontlinesms.welcome=Welkom bij FrontlineSMS! \\o/ +failed.pending.fmessages={0} bericht(en) in behandeling mislukt. Ga naar de berichten in behandeling voor weergave. +language.label=Taal +language.prompt=Verander de taal van de FrontlineSMS gebruikersinterface +frontlinesms.user.support=FrontlineSMS Gebruiker Ondersteuning +download.logs.info1=WARNING: Het FrontlineSMS team kunnen de ingediende logs helaas niet rechtstreeks beantwoorden. Als u een gebruiker ondersteuningsverzoek heeft wordt u verzocht de Help files te raadplegen om te zien of u het antwoord daar kunt vinden. Zoniet, reporteer dan uw probleem via onze gebruikers ondersteuningsforums: +download.logs.info2=Andere gebruikers hebben wellicht hetzelfde probleem gerapporteerd en een oplossing gevonden! Om verder te gaan en uw logs in te dienen, klik 'Doorgaan' +dynamicfield.contact_name.label=Contact Naam +dynamicfield.contact_number.label=Contact Nummer +dynamicfield.keyword.label=Trefwoord +dynamicfield.message_content.label=Bericht Inhoud +# TextMessage domain +fmessage.queued=Bericht is in de wachtrij geplaatst om verzonden te worden naar {0} +fmessage.queued.multiple=Bericht is in de wachtrij geplaatst om verzonden te worden naar {0} ontvangers +fmessage.retry.success=Bericht is opnieuw in de wachtrij geplaatst om verzonden te worden naar {0} +fmessage.retry.success.multiple={0} berichten(en) opnieuw in de wachtrij geplaatst om verzonden te worden +fmessage.displayName.label=Naam +fmessage.text.label=Bericht +fmessage.date.label=Datum +fmessage.to=To: {0} +fmessage.to.multiple=To: {0} ontvangers +fmessage.quickmessage=Send message +fmessage.archive=Archiveren +fmessage.activity.archive=Archiveren {0} +fmessage.unarchive=De-archiveren +fmessage.export=Exporteren +fmessage.rename=Nieuwe naam geven {0} +fmessage.edit=Bewerken {0} +fmessage.delete=Delete +fmessage.moreactions=Meer acties... +fmessage.footer.show=Weergeven +fmessage.footer.show.failed=Mislukt +fmessage.footer.show.all=Alles +fmessage.footer.show.starred=Met ster +fmessage.archive.back=Terug +fmessage.activity.sentmessage=({0} berichten verzonden) +fmessage.failed=mislukt +fmessage.header=berichten +fmessage.section.inbox=Postvak IN +fmessage.section.sent=Verzonden +fmessage.section.pending=In behandeling +fmessage.section.trash=Prullenbak +fmessage.addsender=Aan contacten toevoegen +fmessage.resend=Opnieuw verzenden +fmessage.retry=Opnieuw proberen +fmessage.reply=Beantwoorden +fmessage.forward=Doorsturen +fmessage.messages.none=Er zijn geen berichten hier! +fmessage.selected.none=Er is geen bericht geselecteerd +fmessage.move.to.header=Bericht verplaatsten naar... +fmessage.move.to.inbox=Postvak IN +fmessage.archive.many=Archive selected +fmessage.count=1 bericht +fmessage.count.many={0} berichten +fmessage.many=berichten +fmessage.delete.many=Delete selected +fmessage.reply.many=Reply selected +fmessage.restore=復元 +fmessage.restore.many=Terugzetten +fmessage.retry.many=Retry selected +fmessage.selected.many={0} berichten geselecteerd +fmessage.unarchive.many=Unarchive selected +# TODO move to poll.* +fmessage.showpolldetails=Grafiek weergeven +fmessage.hidepolldetails=Grafiek verbergen +# TODO move to search.* +fmessage.search.none=Geen bericht gevonden +fmessage.search.description=Nieuwe zoekactie links beginnen +activity.name=Naam +activity.delete.prompt=Naar de {0} prullenbak sturen. Alle hieraan gekoppelde berichten zullen nu in de prullenbak geplaatst worden. +activity.label=Activiteit +activity.categorize=Antwoord categoriseren +magicwand.title=Vervangingsexpressies toevoegen +folder.create.success=Map maken succesvol +folder.create.failed=Kon geen map maken +folder.name.validator.error=Naam map reeds in gebruik +folder.name.blank.error=Naam map mag niet leeg zijn +poll.name.blank.error=Poll naam mag niet leeg zijn +poll.name.validator.error=Poll naam reeds in gebruik +autoreply.name.blank.error=Naam automatisch antwoord mag niet leeg zijn +autoreply.name.validator.error=Naam automatisch antwoord reeds in gebruik +announcement.name.blank.error=Naam aankondiging mag niet leeg zijn +announcement.name.validator.error=Naam aankondiging reeds in gebruik +group.name.blank.error=Groepnaam mag niet leeg zijn +group.name.validator.error=Groepnaam reeds in gebruik +#Jquery Validation messages +jquery.validation.required=Dit is een verplicht veld. +jquery.validation.remote=Dit veld corrigeren a.u.b. +jquery.validation.email=Een geldig email adres invullen a.u.b. +jquery.validation.url=Een geldig URL invullen a.u.b. +jquery.validation.date=Een geldige datum invullen a.u.b.. +jquery.validation.dateISO=Een geldige datum invullen a.u.b. (ISO). +jquery.validation.number=Een geldig nummer invullen a.u.b.. +jquery.validation.digits=Uitsluitend nummers invullen a.u.b.. +jquery.validation.creditcard=Een geldig credit card nummer invullen a.u.b.. +jquery.validation.equalto=Dezelfde waarde nogmaals invullen a.u.b.. +jquery.validation.accept=Een waarde met een geldige extensie invullen a.u.b. +jquery.validation.maxlength=Niet meer dan {0} tekens invullen a.u.b. +jquery.validation.minlength=Tenminste {0} tekens invullen a.u.b. +jquery.validation.rangelength=Een waarde tussen {0} en{1} tekenslang invullen a.u.b. +jquery.validation.range=Een waarde tussen {0} en{1} invullen a.u.b. +jquery.validation.max=Een waarde minder dan of gelijk aan {0} invullen a.u.b. +jquery.validation.min=Een waarde meer dan of gelijk aan {0} invullen a.u.b. diff --git a/plugins/frontlinesms-core/grails-app/i18n/messages_km.properties b/plugins/frontlinesms-core/grails-app/i18n/messages_km.properties new file mode 100644 index 000000000..890f7e82e --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/i18n/messages_km.properties @@ -0,0 +1,1065 @@ +# FrontlineSMS English translation by the FrontlineSMS team, Nairobi +language.name=Khmer +# General info +app.version.label=Version +# Common action imperatives - to be used for button labels and similar +action.ok=យល់ព្រម +action.close=បិទ +action.cancel=បោះបង់ +action.done=រួចរាល់ +action.next=បន្ទាប់ +action.prev=មុន +action.back=ថយក្រោយ +action.create=បង្កើត +action.edit=កែសម្រួល +action.rename=ប្ដូរ​ឈ្មោះ +action.save=រក្សាទុក +action.save.all=Save Selected +action.delete=លុប +action.delete.all=Delete Selected +action.send=ផ្ញើ +action.export=នាំចេញ +action.view=View +content.loading=Loading... +# Messages when FrontlineSMS server connection is lost +server.connection.fail.title=បាន​ដាច់ការ​តភ្ជាប់ទៅម៉ាស៊ីន​មេ ។ +server.connection.fail.info=សូម​ចាប់ផ្ដើម FrontlineSMS ឡើង​វិញ ឬ​បិទ​បង្អួច​នេះ ។ +#Connections: +connection.creation.failed=មិន​អាច​ធ្វើ​ការ​តភ្ជាប់ {0} +connection.route.disabled=Deleted connection from {0} to {1}. +connection.route.successNotification=បាន​បង្កើត​ផ្លូវ {0} ដោយ​ជោគជ័យ +connection.route.failNotification=Failed to create connection on {1}: {2} [[[edit]](({0}))]. +connection.route.disableNotification=Disconnected connection on {0} +connection.route.pauseNotification=Paused connection on {0} +connection.route.resumeNotification=Resumed connection on {0} +connection.test.sent=បាន​ផ្ញើ​សារ​សាកល្បង​ដោយ​ជោគជ័យ​ទៅកាន់ {0} ដោយ​ប្រើ {1} +connection.route.exception={1} +# Connection exception messages +connection.error.org.smslib.alreadyconnectedexception=ឧបករណ៍​បាន​ភ្ជាប់​រួចហើយ +connection.error.org.smslib.gsmnetworkregistrationexception=ការ​ចុះឈ្មោះ​ជាមួយ​បណ្ដាញ GSM បាន​បរាជ័យ +connection.error.org.smslib.invalidpinexception=បាន​ផ្ដល់​កូដ PIN មិន​ត្រឹមត្រូវ +connection.error.org.smslib.nopinexception=ទាមទារ​កូដ PIN ប៉ុន្តែ​មិន​បាន​ផ្ដល់ +connection.error.org.smslib.notconnectedexception={0} +connection.error.org.smslib.nosuchportexception=រក​មិន​ឃើញ​ច្រក ឬ​មិន​អាច​ចូល​ដំណើរការ​បាន +connection.error.java.io.ioexception=ច្រក​បាន​បង្ហាញ​កំហុស ៖ {0} +connection.error.frontlinesms2.camel.exception.invalidapiidexception={0} +connection.error.frontlinesms2.camel.exception.authenticationexception={0} +connection.error.frontlinesms2.camel.exception.insufficientcreditexception={0} +connection.error.serial.nosuchportexception=រក​មិន​ឃើញ​ច្រក +connection.error.org.apache.camel.runtimecamelexception=មិន​អាច​តភ្ជាប់ +connection.error.onsave={0} +connection.header=Settings > Connections +connection.list.none=អ្នក​មិន​បាន​កំណត់​រចនាសម្ព័ន្ធ​ការ​តភ្ជាប់ ។ +connection.edit=កែសម្រួល​ការ​តភ្ជាប់ +connection.delete=លុប​ការ​តភ្ជាប់ +connection.deleted=បាន​លុប​ការ​តភ្ជាប់ {0} ។ +connection.route.enable=Enable +connection.route.retryconnection=ព្យាយាម​ម្ដងទៀត +connection.add=បន្ថែម​ការ​តភ្ជាប់​ថ្មី +connection.createtest.message.label=សារ +connection.route.disable=Disable +connection.send.test.message=ផ្ញើ​សារ​សាកល្បង +connection.test.message=សូម​អបអរសាទរ​ពី FrontlineSMS \\o/ អ្នក​បាន​កំណត់​រចនាសម្ព័ន្ធ {0} ដើម្បី​ផ្ញើ​សារ SMS ដោយ​ជោគជ័យ \\o/ +connection.validation.prompt=សូម​បំពេញ​ក្នុង​វាល​ដែល​ទាមទារ​ទាំងអស់ +connection.select=ជ្រើស​ប្រភេទ​តភ្ជាប់ +connection.type=ជ្រើស​ប្រភេទ +connection.details=បញ្ចូល​ព័ត៌មាន​លម្អិត +connection.confirm=បញ្ជាក់ +connection.createtest.number=ចំនួន +connection.confirm.header=បញ្ជាក់​ការ​កំណត់ +connection.name.autoconfigured=បាន​កំណត់​រចនាសម្ព័ន្ធ {0} {1} ដោយ​ស្វ័យប្រវត្តិ​នៅ​លើ​ច្រក {2}" +status.connection.title=ការ​តភ្ជាប់ +status.connection.manage=Manage your connections +status.connection.none=អ្នក​មិន​បាន​កំណត់​រចនាសម្ព័ន្ធ​ការ​តភ្ជាប់ ។ +status.devises.header=បាន​រក​ឃើញ​ឧបករណ៍ +status.detect.modems=រក​ឃើញ​ម៉ូដឹម +status.modems.none=រក​ឧបករណ៍​មិន​ឃើញ​នៅ​ឡើយ ។ +status.header=Usage Statistics +connectionstatus.connecting=កំពុង​តភ្ជាប់ +connectionstatus.connected=បាន​តភ្ជាប់ +connectionstatus.disabled=Disabled +connectionstatus.failed=បាន​បរាជ័យ +connectionstatus.not_connected=មិន​បាន​តភ្ជាប់ +default.doesnt.match.message=លក្ខណសម្បត្តិ [{0}] របស់​ថ្នាក់ [{1}] ដែល​មាន​តម្លៃ [{2}] មិន​ផ្គូផ្គង​ជាមួយ​លំនាំ [{3}] ដែល​បាន​ទាមទារ +default.invalid.url.message=លក្ខណសម្បត្តិ [{0}] របស់​ថ្នាក់ [{1}] ដែល​មាន​តម្លៃ [{2}] គឺជា URL មិន​ត្រឹមត្រូវ +default.invalid.creditCard.message=លក្ខណសម្បត្តិ [{0}] របស់​ថ្នាក់ [{1}] ដែល​មាន​តម្លៃ [{2}] គឺជា​លេខ​ប័ណ្ណ​ឥណទាន​មិន​ត្រឹមត្រូវ +default.invalid.email.message=លក្ខណសម្បត្តិ [{0}] របស់​ថ្នាក់ [{1}] ដែល​មាន​តម្លៃ [{2}] គឺជា​អាសយដ្ឋាន​អ៊ីមែល​មិន​ត្រឹមត្រូវ +default.invalid.range.message=លក្ខណសម្បត្តិ [{0}] របស់​ថ្នាក់ [{1}] ដែល​មាន​តម្លៃ [{2}] មិន​ធ្លាក់​ទៅ​ក្នុង​ជួរ​ត្រឹមត្រូវ​ពី [{3}] ទៅ [{4}] ទេ +default.invalid.size.message=លក្ខណសម្បត្តិ [{0}] របស់​ថ្នាក់ [{1}] ដែល​មាន​តម្លៃ [{2}] មិន​ធ្លាក់​ទៅ​ក្នុង​ទំហំ​ត្រឹមត្រូវ​ពី [{3}] ទៅ [{4}] ទេ +default.invalid.max.message=លក្ខណសម្បត្តិ [{0}] របស់​ថ្នាក់ [{1}] ដែល​មាន​តម្លៃ [{2}] គឺ​លើស​ពី​តម្លៃ​អតិបរមា [{3}] +default.invalid.min.message=លក្ខណសម្បត្តិ [{0}] របស់​ថ្នាក់ [{1}] ដែល​មាន​តម្លៃ [{2}] គឺ​តិច​ជាង​តម្លៃ​អប្បបរមា [{3}] +default.invalid.max.size.message=លក្ខណសម្បត្តិ [{0}] របស់​ថ្នាក់ [{1}] ដែល​មាន​តម្លៃ [{2}] គឺ​លើស​ពី​ទំហំ​អតិបរមា​របស់ [{3}] +default.invalid.min.size.message=លក្ខណសម្បត្តិ [{0}] របស់​ថ្នាក់ [{1}] ដែល​មាន​តម្លៃ [{2}] គឺ​តិច​ជាង​ទំហំ​អប្បបរមា​របស់ [{3}] +default.invalid.validator.message=លក្ខណសម្បត្តិ [{0}] របស់​ថ្នាក់ [{1}] ដែល​មាន​តម្លៃ [{2}] គឺ​មិន​ឆ្លង​ផុត​សពលកម្ម​ផ្ទាល់ខ្លួន​ទេ +default.not.inlist.message=លក្ខណសម្បត្តិ [{0}] របស់​ថ្នាក់ [{1}] ដែល​មាន​តម្លៃ [{2}] គឺ​មិន​មាន​នៅ​ក្នុង​បញ្ជី [{3}] +default.blank.message=លក្ខណសម្បត្តិ [{0}] របស់​ថ្នាក់ [{1}] មិន​អាច​ទទេ​បាន​ឡើយ +default.not.equal.message=លក្ខណសម្បត្តិ [{0}] របស់​ថ្នាក់ [{1}] ដែល​មាន​តម្លៃ [{2}] មិន​អាច​ស្មើ [{3}] បាន​ឡើយ +default.null.message=លក្ខណសម្បត្តិ [{0}] របស់​ថ្នាក់ [{1}] មិន​អាច​គ្មាន​បាន​ទេ +default.not.unique.message=លក្ខណសម្បត្តិ [{0}] របស់​ថ្នាក់ [{1}] ដែល​មាន​តម្លៃ [{2}] ត្រូវតែ​មាន​តែមួយ +default.paginate.prev=មើល​ជាមុន +default.paginate.next=បន្ទាប់ +default.boolean.true=ពិត +default.boolean.false=មិន​ពិត +default.date.format=dd MMMM, yyyy hh:mm +default.number.format=0 +default.unarchived=មិន​បាន​ទុក​ក្នុង​ប័ណ្ណសារ {0} +default.unarchive.failed=ការ​មិន​ទុក​ក្នុង​ប័ណ្ណសារ {0} បាន​បរាជ័យ +default.restored=បាន​ស្ដារ {0} +default.restore.failed=មិន​អាច​ស្ដារ {0} ដែល​មាន​លេខ​សម្គាល់ {1} +default.archived.multiple=បាន​ទុក​ក្នុង​ប័ណ្ណសារ {0} +default.created=បាន​បង្កើត {0} +default.created.message={0} {1} ត្រូវ​បាន​បង្កើត +default.create.failed=បាន​បរាជ័យ​ក្នុង​ការ​បង្កើត {0} +default.updated=បាន​ធ្វើ​បច្ចុប្បន្នភាព {0} +default.update.failed=បាន​បរាជ័យ​ក្នុង​ការ​ធ្វើ​បច្ចុប្បន្នភាព {0} ដែល​មាន​លេខ​សម្គាល់ {1} +default.updated.multiple=បាន​ធ្វើ​បច្ចុប្បន្នភាព {0} +default.updated.message=បាន​ធ្វើ​បច្ចុប្បន្នភាព {0} +default.deleted=បាន​លុប {0} +default.trashed=បាន​ផ្លាស់ទី {0} ទៅ​ធុងសំរាម +default.trashed.multiple=បាន​ផ្លាស់ទី {0} ទៅ​ធុងសំរាម +default.archived=បាន​ទុក​ក្នុង​ប័ណ្ណសារ {0} +default.unarchive.keyword.failed=ការ​មិន​ទុក​ក្នុង​ប័ណ្ណសារ {0} បាន​បរាជ័យ ។ ពាក្យ​គន្លឹះ ឬ​ឈ្មោះ​កំពុង​ត្រូវ​បាន​ប្រើ +default.unarchived.multiple=មិន​បាន​ទុក​ក្នុង​ប័ណ្ណសារ {0} +default.delete.failed=មិន​អាច​លុប {0} ដែល​មាន​លេខ​សម្គាល់ {1} +default.notfound=រក​មិន​ឃើញ {0} ដែល​មាន​លេខ​សម្គាល់ {1} +default.optimistic.locking.failure=មាន​អ្នក​ផ្សេង​ធ្វើ​បច្ចុប្បន្នភាព {0} ពេល​អ្នក​កំពុង​កែសម្រួល​វា +default.home.label=ផ្ទះ +default.list.label=បញ្ជី {0} +default.add.label=បន្ថែម {0} +default.new.label=ថ្មី {0} +default.create.label=បង្កើត {0} +default.show.label=បង្ហាញ {0} +default.edit.label=កែសម្រួល {0} +search.clear=សម្អាត​ការ​ស្វែងរក +default.button.create.label=បង្កើត +default.button.edit.label=កែសម្រួល +default.button.update.label=ធ្វើ​បច្ចុប្បន្នភាព +default.button.delete.label=លុប +default.button.search.label=ស្វែងរក +default.button.apply.label=អនុវត្ត +default.button.delete.confirm.message=តើ​អ្នក​ប្រាកដ​ហើយ​ឬ ? +default.deleted.message=បាន​លុប {0} +# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) +typeMismatch.java.net.URL=លក្ខណសម្បត្តិ {0} ត្រូវតែ​ជា URL ត្រឹមត្រូវ +typeMismatch.java.net.URI=លក្ខណសម្បត្តិ {0} ត្រូវតែ​ជា URI ត្រឹមត្រូវ +typeMismatch.java.util.Date=លក្ខណសម្បត្តិ {0} ត្រូវតែ​ជា​កាលបរិច្ឆេទ​ត្រឹមត្រូវ +typeMismatch.java.lang.Double=លក្ខណសម្បត្តិ {0} ត្រូវតែ​ជា​ចំនួន​ត្រឹមត្រូវ +typeMismatch.java.lang.Integer=លក្ខណសម្បត្តិ {0} ត្រូវតែ​ជា​ចំនួន​ត្រឹមត្រូវ +typeMismatch.java.lang.Long=លក្ខណសម្បត្តិ {0} ត្រូវតែ​ជា​ចំនួន​ត្រឹមត្រូវ +typeMismatch.java.lang.Short=លក្ខណសម្បត្តិ {0} ត្រូវតែ​ជា​ចំនួន​ត្រឹមត្រូវ +typeMismatch.java.math.BigDecimal=លក្ខណសម្បត្តិ {0} ត្រូវតែ​ជា​ចំនួន​ត្រឹមត្រូវ +typeMismatch.java.math.BigInteger=លក្ខណសម្បត្តិ {0} ត្រូវតែ​ជា​ចំនួន​ត្រឹមត្រូវ +typeMismatch.int={0} ត្រូវតែ​ជា​ចំនួន​ត្រឹមត្រូវ +# Application specific messages +messages.trash.confirmation=វា​នឹង​សម្អាត​ធុងសំរាម ហើយ​លុប​ចេញ​ជា​រៀង​រហូត ។ តើ​អ្នក​ចង់​បន្ត​ដែរ​ឬទេ ? +default.created.poll=បាន​បង្កើត​ការ​ស្ទង់​មតិ ! +default.search.label=សម្អាត​ការ​ស្វែងរក +default.search.betweendates.title=រវាង​កាលបរិច្ឆេទ ៖ +default.search.moresearchoption.label=ជម្រើស​ស្វែងរក​ច្រើន​ទៀត +default.search.date.format=d/M/yyyy +default.search.moreoption.label=ជម្រើស​ច្រើន​ទៀត +# SMSLib Fconnection +smslib.label=Phone/Modem +smslib.type.label=ប្រភេទ +smslib.name.label=ឈ្មោះ +smslib.manufacturer.label=Manufacturer +smslib.model.label=Model +smslib.port.label=ច្រក +smslib.baud.label=Baud rate +smslib.pin.label=PIN +smslib.imsi.label=SIM IMSI +smslib.serial.label=Device Serial # +smslib.sendEnabled.label=Use for sending +smslib.receiveEnabled.label=Use for receiving +smslibFconnection.sendEnabled.validator.error.send=Modem should be used for sending +smslibFconnection.receiveEnabled.validator.error.receive=or receiving messages +smslib.description=Connect to USB, serial and bluetooth modems or phones +smslib.global.info=FrontlineSMS will attempt to automatically configure any connected modem or phone, but you can manually configure them here +# Email Fconnection +email.label=អ៊ីមែល +email.type.label=ប្រភេទ +email.name.label=ឈ្មោះ +email.receiveProtocol.label=Protocol +email.serverName.label=ឈ្មោះ​អ្នក​ផ្ញើ +email.serverPort.label=Server Port +email.username.label=Username +email.password.label=Password +# CLickatell Fconnection +clickatell.label=Clickatell +clickatell.type.label=ប្រភេទ +clickatell.name.label=ឈ្មោះ +clickatell.apiId.label=API ID +clickatell.username.label=Username +clickatell.password.label=Password +clickatell.sendToUsa.label=Send to United States +clickatell.fromNumber.label=From number +clickatell.description=Send and receive messages through a Clickatell account. +clickatell.global.info=You will need to configure an account with Clickatell (www.clickatell.com). +clickatellFconnection.fromNumber.validator.invalid=វាល 'ចេញពី​លេខ' ត្រូវ​បាន​ទាមទារ​សម្រាប់​ការ​ផ្ញើ​សារ​ទៅកាន់​សហរដ្ឋអាមេរិក +# TODO: Change markup below to markdown +clickatell.info-local=In order to set up a Clickatell connection, you must first have a Clickatell account. If you do not have one, please go to the Clickatell site and register for a 'Developer's Central Account'. It is free to sign up for test messages, and the process should take less that 5 minutes.

Once you have an active Clickatell account, you will need to 'Create a Connection (API ID)' from the front page. First, select 'APIs,' then select 'Set up a new API.' From there, choose 'add HTTP API' with the default settings, then enter the relevant details below.

The 'Name' field is just for your own reference for your Frontline account, and not related to the Clickatell API, e.g. 'My local message connection'. +clickatell.info-clickatell=The following details should be copied and pasted directly from the Clickatell HTTP API screen. +#Nexmo Fconnection +nexmo.label=Nexmo +nexmo.type.label=Nexmo connection +nexmo.name.label=ឈ្មោះ +nexmo.api_key.label=សោ API ៖ +nexmo.api_secret.label=API secret +nexmo.fromNumber.label=From number +nexmo.description=Send and receive messages through a Nexmo account. +nexmo.receiveEnabled.label=Receiving enabled +nexmo.sendEnabled.label=Sending enabled +# Smssync Fconnection +smssync.label=SMSSync +smssync.name.label=ឈ្មោះ +smssync.type.label=ប្រភេទ +smssync.receiveEnabled.label=Receive enabled +smssync.sendEnabled.label=Send enabled +smssync.secret.label=Secret +smssync.timeout.label=Timeout (mins) +smssync.description=Use an Android phone with the SMSSync app installed to send and receive SMS using Frontline products. +smssync.field.secret.info=On your app, set the secret to match this field. +smssync.global.info=Download and install [SMSSync from the Android App store](https://play.google.com/store/apps/details?id=org.addhen.smssync&hl=en) to your Android phone. +smssync.timeout=The Android phone associated with "{0}" has not contacted your Frontline account for {1} minute(s) [edit] +smssync.info-setup=Frontline products enable you to send and receive messages through your Android phone. In order to do this you will need to:\n\n1. Input a 'Secret' and name your connection. A secret is simply a password of your choice.\n2. Download and install [SMSSync from the Android App store](https://play.google.com/store/apps/details?id=org.addhen.smssync&hl=en) to your Android phone\n3. Once you have created this connection, you can create a new Sync URL within SMSSync on your Android phone by entering the connection URL (generated by your Frontline product and displayed on the next page) and your chosen secret. See [The SMSSync Site](http://smssync.ushahidi.com/howto) for more help. +smssync.info-timeout=If SMSSync does not contact your Frontline product for a certain duration (default 60 minutes), your queued messages will NOT be sent, and you will see a notification that the messages failed to send. Select this duration below: +smssync.info-name=Finally, you should name your SMSSync connection with a name of your choice, e.g. 'Bob's work Android'. +# Messages Tab +message.create.prompt=បញ្ចូល​សារ +message.character.count=តួអក្សរ​នៅសល់ {0} (សារ SMS {1}) +message.character.count.warning=អាច​នឹង​ប្រើ​ពេល​​យូរ បន្ទាប់ពី​ដំណើរការ​ជំនួស +message.header.inbox=ប្រអប់​ទទួល +message.header.sent=បាន​ផ្ញើ +message.header.pending=មិនទាន់​សម្រេច +message.header.trash=ធុងសំរាម +message.header.folder=ថត +message.header.activityList=បញ្ជី​សកម្មភាព +message.header.folderList=បញ្ជី​ថត +announcement.label=សេចក្ដី​ប្រកាស +announcement.description=ផ្ញើ​សារ​សេចក្ដី​ប្រកាស និង​គ្រប់គ្រង​ចម្លើយ​តប +announcement.info1=បាន​រក្សាទុក​សេចក្ដី​ប្រកាស ហើយ​សារ​ត្រូវ​បាន​បន្ថែម​ទៅ​ក្នុង​ជួរ​សារ​មិន​ទាន់​សម្រេច ។ +announcement.info2=វា​អាច​ប្រើ​ពេល​បន្តិចបន្តួច​ដើម្បី​ផ្ញើ​សារ​ទាំងអស់​ចេញ គឺ​វា​អាស្រ័យ​លើ​ចំនួន​សារ និង​ការ​តភ្ជាប់​បណ្ដាញ ។ +announcement.info3=ដើម្បី​មើល​ស្ថានភាព​សារ​របស់​អ្នក បើក​ថត​សារ 'មិន​ទាន់​សម្រេច' ។ +announcement.info4=ដើម្បី​មើល​សេចក្ដី​ប្រកាស ចុច​លើ​វា​ក្នុង​ម៉ឺនុយ​ខាងឆ្វេង​ដៃ ។ +announcement.validation.prompt=សូម​បំពេញ​ក្នុង​វាល​ដែល​បាន​ទាមទារ​ទាំងអស់ +announcement.select.recipients=ជ្រើស​អ្នក​ទទួល +announcement.confirm=បញ្ជាក់ +announcement.delete.warn=លុប {0} ព្រមាន ៖ មិន​អាច​មិនធ្វើវិញ​បាន​ទេ ! +announcement.prompt=ដាក់​ឈ្មោះ​សេចក្ដី​ប្រកាស​នេះ +announcement.confirm.message=សារ +announcement.details.label=ការ​បញ្ជាក់​លម្អិត +announcement.message.label=សារ +announcement.message.none=គ្មាន +announcement.recipients.label=អ្នក​ទទួល +announcement.create.message=បង្កើត​សារ +#TODO embed javascript values +announcement.recipients.count=បាន​ជ្រើស​ទំនាក់ទំនង +announcement.messages.count=សារ​នឹង​ត្រូវ​បាន​ផ្ញើ +announcement.moreactions.delete=លុប​សេចក្ដី​ប្រកាស +announcement.moreactions.rename=ប្ដូរ​ឈ្មោះ​សេចក្ដី​ប្រកាស +announcement.moreactions.edit=កែសម្រួល​សេចក្ដី​ប្រកាស +announcement.moreactions.export=នាំចេញ​សេចក្ដី​ប្រកាស +frontlinesms2.Announcement.name.unique.error.frontlinesms2.Announcement.name={1} {0} "{2}" ត្រូវតែ​មានតែ​មួយ +archive.inbox=ប័ណ្ណសារ​ប្រអប់​ទទួល +archive.sent=ប័ណ្ណសារ​បាន​ផ្ញើ +archive.activity=ប័ណ្ណសារ​សកម្មភាព +archive.folder=ប័ណ្ណសារ​ថត +archive.folder.name=ឈ្មោះ +archive.folder.date=កាលបរិច្ឆេទ +archive.folder.messages=សារ +archive.folder.none=  គ្មាន​ថត​ប័ណ្ណសារ +archive.activity.name=ឈ្មោះ +archive.activity.type=ប្រភេទ +archive.activity.date=កាលបរិច្ឆេទ +archive.activity.messages=សារ +archive.activity.list.none=  គ្មាន​សកម្មភាព​ប័ណ្ណសារ +archive.header=ប័ណ្ណសារ +autoreply.enter.keyword=បញ្ចូល​ពាក្យ​គន្លឹះ +autoreply.create.message=បញ្ចូល​សារ​ឆ្លើយតប​ស្វ័យប្រវត្តិ +activity.autoreply.sort.description=ប្រសិនបើ​មនុស្ស​ផ្ញើ​សារ​ដែល​ចាប់ផ្ដើម​ដោយ​ពាក្យ​គន្លឹះ​ជាក់លាក់, FrontlineSMS អាច​ដំណើរការ​សារ​ដោយ​​​​ស្វ័យប្រវត្តិ​នៅ​លើ​ប្រព័ន្ធ​របស់​អ្នក ។ +activity.autoreply.disable.sorting.description=សារ​នឹង​មិន​ត្រូវ​បាន​ផ្លាស់ទី​ដោយ​ស្វ័យប្រវត្តិ​ទៅ​ក្នុង​សកម្មភាព​នេះ​ទេ ហើយ​វា​បាន​ឆ្លើយតប​ទៅ +autoreply.confirm=បញ្ជាក់ +autoreply.name.label=សារ +autoreply.details.label=ការ​បញ្ជាក់​លម្អិត +autoreply.label=ឆ្លើយតប​ស្វ័យប្រវត្តិ +autoreply.keyword.label=ពាក្យ​គន្លឹះ +autoreply.description=ឆ្លើយតប​ស្វ័យប្រវត្តិ​ទៅកាន់​សារ​ចូល +autoreply.info=បាន​បង្កើត​ចម្លើយ​តប​​​​ស្វ័យប្រវត្តិ, សារ​ណាមួយ​ដែល​មាន​ពាក្យ​គន្លឹះ​របស់​អ្នក​នឹង​ត្រូវ​បាន​បន្ថែម​ទៅកាន់​សកម្មភាព​ឆ្លើយតប​ស្វ័យប្រវត្តិ ដែល​អាច​ត្រូវ​បាន​ពិនិត្យ​ឡើងវិញ​ដោយ​ចុច​លើ​វា​នៅ​ក្នុង​ម៉ឺនុយ​ខាង​​​ស្ដាំ​ដៃ ។ +autoreply.info.warning=ចម្លើយ​តប​ដែល​គ្មាន​ពាក្យ​គន្លឹះ​នឹង​ត្រូវ​បាន​ផ្ញើ​ទៅកាន់​សារ​ចូល​ទាំងអស់ +autoreply.info.note=ចំណាំ ៖ ប្រសិនបើ​អ្នក​ទុក​ចម្លើយ​តប​នៅ​ក្នុង​ប័ណ្ណសារ នោះ​សារ​ចូល​នឹង​លែង​ត្រូវ​បាន​តម្រៀប​ទៀត​ហើយ ។ +autoreply.validation.prompt=សូម​បំពេញ​នៅ​ក្នុង​វាល​ដែល​បាន​ទាមទារ​ទាំងអស់ +autoreply.message.title=សារ​ដែល​ត្រូវ​ផ្ញើ​ត្រឡប់​សម្រាប់​ចម្លើយ​តប​ស្វ័យប្រវត្តិ​នេះ ៖ +autoreply.keyword.title=តម្រៀប​សារ​ស្វ័យប្រវត្តិ​ដោយ​ប្រើ​ពាក្យ​គន្លឹះ ៖ +autoreply.name.prompt=ដាក់​ឈ្មោះ​ចម្លើយ​តប​នេះ +autoreply.message.count=០ តួ​អក្សរ (សារ SMS ១) +autoreply.moreactions.delete=លុប​ចម្លើយ​តប​ស្វ័យប្រវត្តិ +autoreply.moreactions.rename=ប្ដូរ​ឈ្មោះ​ចម្លើយ​តប​ស្វ័យប្រវត្តិ +autoreply.moreactions.edit=កែ​ចម្លើយ​តប​ស្វ័យប្រវត្តិ +autoreply.moreactions.export=នាំចេញ​ចម្លើយ​តប​ស្វ័យប្រវត្តិ +autoreply.all.messages=កុំ​ប្រើ​ពាក្យ​គន្លឹះ (សារ​ចូល​ទាំងអស់​នឹង​ទទួល​ចម្លើយ​តប​ស្វ័យប្រវត្តិ​នេះ) +autoreply.text.none=គ្មាន +frontlinesms2.Autoreply.name.unique.error.frontlinesms2.Autoreply.name={1} {0} "{2}" ត្រូវតែ​មានតែ​មួយ +frontlinesms2.Autoreply.name.validator.error.frontlinesms2.Autoreply.name=ឈ្មោះ​ចម្លើយ​តប​ស្វ័យប្រវត្តិ​ត្រូវតែ​មានតែ​មួយ +frontlinesms2.Keyword.value.validator.error.frontlinesms2.Autoreply.keyword.value=ពាក្យ​គន្លឹះ "{2}" គឺ​បាន​ប្រើ​រួចហើយ +frontlinesms2.Autoreply.autoreplyText.nullable.error.frontlinesms2.Autoreply.autoreplyText=សារ​មិន​អាច​ទទេ​បាន​ឡើយ +autoforward.title={0} +autoforward.label=ការ​បញ្ជូន​បន្ត​ស្វ័យប្រវត្តិ +autoforward.description=បញ្ជូន​បន្ត​សារ​ចូល​ទៅកាន់​ទំនាក់ទំនង​ដោយ​ស្វ័យប្រវត្តិ +autoforward.recipientcount.current=អ្នក​ទទួល​បច្ចុប្បន្ន {0} +autoforward.create.message=បញ្ចូល​សារ +autoforward.confirm=បញ្ជាក់ +autoforward.recipients=អ្នក​ទទួល +autoforward.name.prompt=ដាក់ឈ្មោះ​សារ​បញ្ជូន​បន្ត​ស្វ័យប្រវត្តិ​នេះ +autoforward.details.label=ការ​បញ្ជាក់​លម្អិត +autoforward.keyword.label=ពាក្យ​គន្លឹះ +autoforward.name.label=សារ +autoforward.contacts=ទំនាក់ទំនង +autoforward.groups=ក្រុម +autoforward.info=បាន​បង្កើត​ការ​បញ្ជូន​បន្ត​​ស្វ័យប្រវត្តិ សារ​ណាមួយ​ដែល​មាន​ពាក្យ​គន្លឹះ​របស់​អ្នក នឹង​ត្រូវ​បាន​បន្ថែម​ទៅកាន់​សកម្មភាព​បញ្ជូន​បន្ត​ស្វ័យប្រវត្តិ​នេះ ហើយ​អាច​ត្រូវ​បាន​ពិនិត្យ​ឡើងវិញ​ដោយ​ចុច​លើ​វា​នៅ​ក្នុង​ម៉ឺនុយ​ខាងស្ដាំ​ដៃ ។ +autoforward.info.warning=ការ​បញ្ជូន​បន្ត​ស្វ័យប្រវត្តិ​ដែល​គ្មាន​ពាក្យ​គន្លឹះ នឹង​មាន​លទ្ធផល​នៅ​ក្នុង​សារ​ចូល​​ទាំងអស់​ដែល​ដែល​កំពុង​ត្រូវ​បាន​បញ្ជូន​បន្ត +autoforward.info.note=ចំណាំ ៖ ប្រសិនបើ​អ្នក​ទុក​ការ​បញ្ជូន​បន្ត​ស្វ័យប្រវត្តិ​ក្នុង​ប័ណ្ណសារ នោះ​សារ​ចូល​នឹង​លែង​ត្រូវ​បាន​​​តម្រៀប​ទៀត​ហើយ ។ +autoforward.save=បាន​រក្សាទុក​ការ​បញ្ជូន​បន្ត ! +autoforward.save.success=បាន​រក្សាទុក​ការ​បញ្ជូន​បន្ត ! {0} +autoforward.global.keyword=គ្មាន (សារ​ចូល​ទាំងអស់​នឹង​ត្រូវ​បាន​ដំណើរការ) +autoforward.disabled.keyword=គ្មាន (បាន​បិទ​ការ​តម្រៀប​ស្វ័យប្រវត្តិ) +autoforward.keyword.none.generic=គ្មាន +autoforward.groups.none=គ្មាន +autoforward.contacts.none=គ្មាន +autoforward.message.format=សារ +contact.new=ទំនាក់ទំនង​ថ្មី +contact.list.no.contact=គ្មាន​ទំនាក់ទំនង ! +contact.header=ទំនាក់ទំនង +contact.header.group=ទំនាក់ទំនង >> {0} +contact.all.contacts=ទំនាក់ទំនង​ទាំងអស់ ({0}) +contact.create=បង្កើត​ទំនាក់ទំនង​ថ្មី +contact.groups.header=ក្រុម +contact.create.group=បង្កើត​ក្រុម​ថ្មី +contact.smartgroup.header=ក្រុម​វៃ​ឆ្លាត +contact.create.smartgroup=បង្កើត​ក្រុម​វៃ​ឆ្លាត​ថ្មី +contact.add.to.group=បន្ថែម​ទៅ​ក្រុម... +contact.remove.from.group=លុបចេញ​ពី​ក្រុម +contact.customfield.addmoreinformation=បន្ថែម​ព័ត៌មាន​លម្អិត... +contact.customfield.option.createnew=បង្កើត​ថ្មី... +contact.name.label=ឈ្មោះ +contact.phonenumber.label=លេខ​ទូរស័ព្ទ +contact.notes.label=ចំណាំ +contact.email.label=អ៊ីមែល +contact.groups.label=ក្រុម +contact.notinanygroup.label=មិនមែន​ជា​ផ្នែក​នៃ​ក្រុម​ណាមួយ +contact.messages.label=សារ +contact.messages.sent=បាន​ផ្ញើ​សារ {0} +contact.received.messages=បាន​ទទួល​សារ {0} +contact.search.messages=ស្វែងរក​សារ +contact.select.all=ជ្រើស​ទាំងអស់ +contact.search.placeholder=Search your contacts, or enter phone numbers +contact.search.contact=ទំនាក់ទំនង +contact.search.smartgroup=ក្រុម​វៃ​ឆ្លាត +contact.search.group=ក្រុម +contact.search.address=បន្ថែម​លេខ​ទូរស័ព្ទ ៖ +contact.not.found=រក​មិន​ឃើញ​ទំនាក់ទំនង +group.not.found=រក​មិន​ឃើញ​ក្រុម +smartgroup.not.found=រក​មិន​ឃើញ​ក្រុម​វៃ​ឆ្លាត +group.rename=ប្ដូរ​ឈ្មោះ​ក្រុម +group.edit=កែសម្រួល​ក្រុម +group.delete=លុប​ក្រុម +group.moreactions=សកម្មភាព​ច្រើន​ទៀត... +customfield.validation.prompt=សូម​បញ្ចូល​ឈ្មោះ +customfield.validation.error=ឈ្មោះ​មាន​រួចហើយ +customfield.name.label=ឈ្មោះ +export.contact.info=ដើម្បី​នាំចេញ​ទំនាក់ទំនង​ពី FrontlineSMS, ជ្រើស​ប្រភេទ​នាំចេញ និង​ព័ត៌មាន​ដែល​ត្រូវ​រួម​បញ្ចូល​នៅ​ក្នុង​ទិន្នន័យ​នាំចេញ ។ +export.message.info=ដើម្បី​នាំចេញ​សារពី FrontlineSMS, ជ្រើស​ប្រភេទ​នាំចេញ និង​ព័ត៌មាន​ដែល​ត្រូវ​រួម​បញ្ចូល​នៅ​ក្នុង​ទិន្នន័យ​នាំចេញ ។ +export.selectformat=ជ្រើស​ទ្រង់ទ្រាយ​បញ្ចេញ +export.csv=ទ្រង់ទ្រាយ CSV សម្រាប់​ប្រើ​ក្នុង​សៀវភៅ​បញ្ជី +export.pdf=ទ្រង់ទ្រាយ PDF សម្រាប់​បោះពុម្ព +folder.name.label=ឈ្មោះ +group.delete.prompt=តើ​អ្នក​ពិតជា​ចង់​លុប {0} មែន​ឬ ? ព្រមាន ៖ វា​មិន​អាច​មិនធ្វើវិញ​បាន​ទេ +layout.settings.header=ការ​កំណត់ +activities.header=សកម្មភាព +activities.create=បង្កើត​សកម្មភាព​ថ្មី +folder.header=ថត +folder.create=បង្កើត​ថត​ថ្មី +folder.label=ថត +message.folder.header=ថត {0} +fmessage.trash.actions=សកម្មភាព​ធុងសំរាម... +fmessage.trash.empty=សម្អាត​ធុងសំរាម +fmessage.to.label=ជូន​ចំពោះ +trash.empty.prompt=នឹង​លុប​សកម្មភាព និង​សារ​ទាំងអស់​នៅ​ក្នុង​ធុងសំរាម​ជា​រៀង​រហូត +fmessage.responses.total=ចម្លើយ​តប​សរុប {0} +fmessage.label=សារ +fmessage.label.multiple=សារ {0} +poll.prompt=ដាក់ឈ្មោះ​ការ​ស្ទង់​មតិ​នេះ +poll.details.label=ការ​បញ្ជាក់​លម្អិត +poll.message.label=សារ +poll.choice.validation.error.deleting.response=ជម្រើស​ដែល​បាន​រក្សាទុក​មិន​អាច​មាន​តម្លៃ​ទទេ​បាន​ឡើយ +poll.alias=ឈ្មោះ​ក្លែងក្លាយ +poll.keywords=ពាក្យ​គន្លឹះ +poll.aliases.prompt=បញ្ចូល​ឈ្មោះ​ក្លែងក្លាយ​សម្រាប់​ជម្រើស​ឆ្លើយតប ។ +poll.keywords.prompt.details=ពាក្យ​គន្លឹះ​កម្រិត​ខ្ពស់​នឹង​​ដាក់ឈ្មោះ​ឲ្យ​ការ​ស្ទង់​មតិ ហើយ​វា​ត្រូវ​បាន​ផ្ញើ​នៅ​ក្នុង​សារ​សេចក្ដី​ណែនាំ ។ ចម្លើយ​តប​នីមួយៗ​អាច​មាន​ពាក្យ​​​គន្លឹះ​ផ្លូវ​កាត់​ផ្សេង​គ្នា ។ +poll.keywords.prompt.more.details=អ្នក​អាច​បញ្ចូល​ពាក្យ​គន្លឹះ​ច្រើន​​បាន​ដោយ​បំបែក​ដោយ​សញ្ញា (,) សម្រាប់​កម្រិត​ខ្ពស់ និង​ចម្លើយតប ។ ប្រសិនបើ​មិន​បាន​បញ្ចូល​ពាក្យ​​​គន្លឹះ​កម្រិត​ខ្ពស់​នៅ​ខាងក្រោម​ទេ នោះ​ពាក្យ​គន្លឹះ​ឆ្លើយតប​ទាំងនេះ​ចាំបាច់​ត្រូវតែ​មាន​តែមួយ​សម្រាប់​គ្រប់​សកម្មភាព​ទាំងអស់ ។ +poll.keywords.response.label=ពាក្យ​គន្លឹះ​ឆ្លើយ​តប +poll.response.keyword=កំណត់​ពាក្យ​គន្លឹះ​ឆ្លើយតប +poll.set.keyword=កំណត់​ពាក្យ​គន្លឹះ​កម្រិត​ខ្ពស់ +poll.keywords.validation.error=ពាក្យ​គន្លឹះ​គួរ​មានតែ​មួយ +poll.sort.label=តម្រៀប​ស្វ័យប្រវត្តិ +poll.autosort.no.description=កុំ​តម្រៀប​ចម្លើយ​ដោយ​ស្វ័យប្រវត្តិ +poll.autosort.description=តម្រៀប​ចម្លើយ​ដោយ​ស្វ័យប្រវត្តិ ។ +poll.sort.keyword=ពាក្យ​គន្លឹះ +poll.sort.toplevel.keyword.label=ពាក្យ​គន្លឹះ​កម្រិត​ខ្ពស់ (ជា​ជម្រើស) +poll.sort.by=តម្រៀប​តាម +poll.autoreply.label=ឆ្លើយតប​ស្វ័យប្រវត្តិ +poll.autoreply.none=គ្មាន +poll.recipients.label=អ្នក​ទទួល +poll.recipients.none=គ្មាន +poll.toplevelkeyword=ពាក្យ​គន្លឹះ​កម្រិត​ខ្ពស់ +poll.sort.example.toplevel=ឧ. ក្រុម +poll.sort.example.keywords.A=e.g A, AMAZING +poll.sort.example.keywords.B=e.g B, BEAUTIFUL +poll.sort.example.keywords.C=e.g C, COAURAGEOUS +poll.sort.example.keywords.D=e.g D, DELIGHTFUL +poll.sort.example.keywords.E=e.g E, EXEMPLARY +poll.sort.example.keywords.yn.A=e.g YES, YAP +poll.sort.example.keywords.yn.B=e.g No, NOP +#TODO embed javascript values +poll.recipients.count=បាន​ជ្រើស​ទំនាក់ទំនង +poll.messages.count=សារ​នឹង​ត្រូវ​បាន​ផ្ញើ +poll.yes=បាទ/ចាស +poll.no=ទេ +poll.label=ស្ទង់​មតិ +poll.description=ផ្ញើ​សំណួរ និង​ការ​វិភាគ​ចម្លើយ +poll.messages.sent=បាន​ផ្ញើ​សារ {0} +poll.response.enabled=បាន​បើក​ចម្លើយ​តប​ស្វ័យប្រវត្តិ +poll.message.edit=កែ​សារ​ដែល​ត្រូវ​ផ្ញើ​ទៅ​អ្នក​ទទួល +poll.message.prompt=សារ​ខាងក្រោម​នឹង​ត្រូវ​ផ្ញើ​ទៅ​អ្នក​នៃ​ការ​ស្ទង់​មតិ +poll.message.count=តួអក្សរ​នៅសល់ ១៦០ (សារ SMS ១) +poll.moreactions.delete=លុប​ការ​ស្ទង់​មតិ +poll.moreactions.rename=ប្ដូរ​ឈ្មោះ​ការ​ស្ទង់​មតិ +poll.moreactions.edit=កែ​ការ​ស្ទង់​មតិ +poll.moreactions.export=នាំចេញ​ការ​ស្ទង់​មតិ +folder.moreactions.delete=លុប​ថត +folder.moreactions.rename=ប្ដូរ​ឈ្មោះ​ថត +folder.moreactions.export=នាំចេញ​ថត +#TODO embed javascript values +poll.reply.text=ឆ្លើយ​ថា "{0}" សម្រាប់​បាទ/ចាស, "{1}" សម្រាប់​ទេ ។ +poll.reply.text1={0} "{1}" សម្រាប់ {2} +poll.reply.text2=សូម​ឆ្លើយ 'បាទ/ចាស' ឬ 'ទេ' +poll.reply.text3=ឬ +poll.reply.text5=ឆ្លើយតប +poll.reply.text6=សូម​ឆ្លើយ +poll.message.send={0} {1} +poll.recipients.validation.error=ជ្រើស​ទំនាក់ទំនង​ដើម្បី​ផ្ញើ​សារ +frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name={1} {0} "{2}" ត្រូវតែ​មានតែ​មួយ +frontlinesms2.Poll.responses.validator.error.frontlinesms2.Poll.responses=ជម្រើស​ឆ្លើយតប​មិន​អាច​ដូចគ្នា​បាន​ទេ +frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value=ពាក្យ​គន្លឹះ "{2}" ត្រូវ​បាន​ប្រើ​រួចហើយ +wizard.title.new=ថ្មី +wizard.fmessage.edit.title=កែសម្រួល {0} +popup.title.saved=បាន​រក្សាទុក {0} ! +popup.activity.create=បង្កើត​សកម្មភាព​ថ្មី ៖ ជ្រើស​ប្រភេទ +popup.smartgroup.create=បង្កើត​ក្រុម​វៃ​ឆ្លាត +popup.help.title=ជំនួយ +smallpopup.customfield.create.title=បង្កើត​វាល​ផ្ទាល់ខ្លួន +smallpopup.group.rename.title=ប្ដូរ​ឈ្មោះ​ក្រុម +smallpopup.group.edit.title=កែសម្រួល​ក្រុម +smallpopup.group.delete.title=លុប​ក្រុម +smallpopup.fmessage.rename.title=ប្ដូរ​ឈ្មោះ {0} +smallpopup.fmessage.delete.title=លុប {0} +smallpopup.fmessage.export.title=នាំចេញ +smallpopup.delete.prompt=លុប {0} ? +smallpopup.delete.many.prompt=លុប​ទំនាក់ទំនង {0} ? +smallpopup.empty.trash.prompt=សម្អាត​ធុងសំរាម? +smallpopup.messages.export.title=នាំចេញ​លទ្ធផល (សារ {0}) +smallpopup.test.message.title=សារ​សាកល្បង +smallpopup.recipients.title=អ្នក​ទទួល +smallpopup.folder.title=ថត +smallpopup.contact.export.title=នាំចេញ +smallpopup.contact.delete.title=លុប +contact.selected.many=បាន​ជ្រើស​ទំនាក់ទំនង {0} +group.join.reply.message=សូម​ស្វាគមន៍ +group.leave.reply.message=Bye +fmessage.new.info=អ្នក​មាន​សារ​ថ្មី {0} ។ ចុច​ដើម្បី​មើល +wizard.quickmessage.title=Send Message +wizard.messages.replyall.title=ឆ្លើយតប​ទាំងអស់ +wizard.send.message.title=ផ្ញើ​សារ +wizard.ok=យល់ព្រម +wizard.create=បង្កើត +wizard.save=រក្សាទុក +wizard.send=ផ្ញើ +common.settings=ការ​កំណត់ +common.help=ជំនួយ +validation.nospaces.error=ពាក្យ​គន្លឹះ​មិន​គួរ​មាន​ដកឃ្លា​ទេ +activity.validation.prompt=សូម​បំពេញ​វាល​ដែល​បាន​ទាមទារ​ទាំងអស់ +validator.invalid.name=Another activity exists with the name {2} +autoreply.blank.keyword=ពាក្យ​គន្លឹះ​ទទេ ។ ការ​ឆ្លើយតប​នឹង​ត្រូវ​ផ្ញើ​ទៅកាន់​សារ​ចូល​ទាំងអស់ +poll.type.prompt=ជ្រើស​ប្រភេទ​ស្ទង់​មតិ​ដើម្បី​បង្កើត +poll.question.yes.no=សំណួរ​ចម្លើយ 'បាទ/ចាស' ឬ 'ទេ' +poll.question.multiple=សំណួរ​ពហុ​ជ្រើសរើស (ឧ. 'ក្រហម', 'ខៀវ', 'បៃតង') +poll.question.prompt=បញ្ចូល​សំណួរ +poll.message.none=កុំ​ផ្ញើ​សារ​សម្រាប់​ការ​ស្ទង់​មតិ​នេះ (ប្រមូល​តែ​ចម្លើយ​ប៉ុណ្ណោះ) ។ +poll.replies.header=ឆ្លើយតប​ស្វ័យប្រវត្តិ​ទៅកាន់​ចម្លើយ​ការ​ស្ទង់​មតិ (ជា​ជម្រើស) +poll.replies.description=នៅ​ពេល​បាន​កំណត់​សារ​ចូល​ជា​ចម្លើយ​តប​ការ​ស្ទង់​មតិ នោះ​វា​នឹង​ផ្ញើ​សារ​ទៅកាន់​មនុស្ស​ដែល​បាន​ផ្ញើ​ចម្លើយ​តប ។ +poll.autoreply.send=ផ្ញើ​ចម្លើយ​តប​ស្វ័យប្រវត្តិ​ទៅ​ចម្លើយ​តប​ការ​ស្ទង់​មតិ +poll.responses.prompt=បញ្ចូល​ចម្លើយ​តប​ដែល​អាច​កើត​មាន (ចន្លោះ​រវាង ២ និង ៥) +poll.sort.header=តម្រៀប​សារ​ស្វ័យប្រវត្ត​ដោយ​ប្រើ​ពាក្យ​គន្លឹះ (ជា​ជម្រើស) +poll.sort.enter.keywords=បញ្ចូល​ពាក្យ​គន្លឹះ​សម្រាប់​ការ​ស្ទង់​មតិ និង​ចម្លើយ​តប +poll.sort.description=ប្រសិនបើ​អ្នកប្រើ​ផ្ញើ​ចម្លើយ​តប​ការ​ស្ទង់​មតិ​ដោយ​ប្រើ​ពាក្យ​​​​គន្លឹះ​ជាក់លាក់ FrontlineSMS អាច​តម្រៀប​សារ​ដោយ​ស្វ័យប្រវត្តិ​នៅ​លើ​ប្រព័ន្ធ​របស់​អ្នក ។ +poll.no.automatic.sort=កុំ​តម្រៀប​សារ​ដោយ​ស្វ័យប្រវត្តិ +poll.sort.automatically=តម្រៀប​សារ​ដែល​មាន​ពាក្យ​គន្លឹះ​ដូច​ខាងក្រោម​ដោយ​ស្វ័យប្រវត្តិ +poll.validation.prompt=សូម​បំពេញ​ដែល​ទាមទារ​ទាំងអស់ +poll.name.validator.error.name=ឈ្មោះ​ការ​ស្ទង់​មតិ​ត្រូវ​មានតែ​មួយ +pollResponse.value.blank.value=តម្លៃ​ចម្លើយ​តប​ការ​ស្ទង់​មតិ​មិន​អាច​ទទេ​បាន​ឡើយ +poll.keywords.validation.error.invalid.keyword=ពាក្យ​គន្លឹះ​មិន​ត្រឹមត្រូវ ។ សូម​បញ្ចូល​ជា​ឈ្មោះ ឬ​ពាក្យ +poll.question=បញ្ចូល​សំណួរ +poll.response=បញ្ជី​ឆ្លើយតប +poll.sort=ការ​តម្រៀប​ស្វ័យប្រវត្តិ +poll.reply=ឆ្លើយតប​ស្វ័យប្រវត្តិ +poll.edit.message=កែសម្រួល​សារ +poll.recipients=ជ្រើស​អ្នក​ទទួល +poll.confirm=បញ្ជាក់ +poll.save=បាន​រក្សាទុក​ការ​ស្ទង់​មតិ ! +poll.save.success=បាន​រក្សាទុក​ការ​ស្ទង់​មតិ {0} ! +poll.messages.queue=ប្រសិនបើ​អ្នក​ជ្រើស​យក​​ផ្ញើ​សារ​ដោយ​ប្រើ​ការ​ស្ទង់​មតិ​នេះ នោះ​សារ​គឺ​ត្រូវ​បាន​បន្ថែម​​ទៅកាន់​ជួរ​សារ​មិនទាន់​សម្រេច ។ +poll.messages.queue.status=វា​អាច​ប្រើ​ពេល​បន្តិចបន្តួច​​​ដើម្បី​ផ្ញើ​សារ​ទាំងអស់​ចេញ គឺ​វា​អាស្រ័យ​លើ​ចំនួន​សារ និង​ការ​តភ្ជាប់​បណ្ដាញ ។ +poll.pending.messages=ដើម្បី​មើល​ស្ថានភាព​សារ​របស់​អ្នក អ្នក​ត្រូវ​បើក​ថត​សារ 'មិនទាន់​សម្រេច' ។ +poll.send.messages.none=នឹង​គ្មាន​សារ​ត្រូវ​បាន​ផ្ញើ​ឡើយ +quickmessage.details.label=ការ​បញ្ជាក់​លម្អិត +quickmessage.message.label=សារ +quickmessage.message.none=គ្មាន +quickmessage.recipient.label=អ្នក​ទទួល +quickmessage.recipients.label=អ្នក​ទទួល +quickmessage.message.count=តួអក្សរ​នៅសល់ ១៦០ (សារ SMS ១) +quickmessage.enter.message=បញ្ចូល​សារ +quickmessage.select.recipients=ជ្រើស​អ្នក​ទទួល +quickmessage.confirm=បញ្ជាក់ +#TODO embed javascript values +quickmessage.recipients.count=បាន​ជ្រើស​ទំនាក់ទំនង +quickmessage.messages.count=សារ​នឹង​ត្រូវ​បាន​ផ្ញើ +quickmessage.count.label=ចំនួន​សារ ៖ +quickmessage.messages.label=បញ្ចូល​សារ +quickmessage.phonenumber.label=បន្ថែម​លេខ​ទូរស័ព្ទ ៖ +quickmessage.phonenumber.add=បន្ថែម +quickmessage.selected.recipients=បាន​ជ្រើស​អ្នក​ទទួល +quickmessage.validation.prompt=សូម​បំពេញ​ដែល​ទាមទារ​ទាំងអស់ +fmessage.number.error=Non-numeric characters in this field will be removed when saved +search.filter.label=កំណត់​ព្រំដែន​ការ​ស្វែងរក​ទៅ +search.filter.group=ជ្រើស​ក្រុម +search.filter.activities=ជ្រើស​សកម្មភាព/ថត +search.filter.messages.all=ដែល​បាន​ផ្ញើ និង​ទទួល​ទាំងអស់ +search.filter.inbox=តែ​សារ​ដែល​បាន​ទទួល​ប៉ុណ្ណោះ +search.filter.sent=តែ​សារ​ដែល​បាន​ផ្ញើ​ប៉ុណ្ណោះ +search.filter.archive=រួម​ទាំង​ប័ណ្ណសារ +search.betweendates.label=រវាង​កាលបរិច្ឆេទ +search.header=ស្វែងរក +search.quickmessage=Send message +search.export=នាំចេញ​លទ្ធផល +search.keyword.label=ពាក្យ​គន្លឹះ +search.contact.name.label=ឈ្មោះ​ទំនាក់ទំនង +search.contact.name=ឈ្មោះ​ទំនាក់ទំនង +search.result.header=លទ្ធផល +search.moreoptions.label=ជម្រើស​ច្រើន​ទៀត +settings.general=ទូទៅ +settings.porting=Import and Export +settings.connections=ទូរស័ព្ទ & ការ​តភ្ជាប់ +settings.logs=ប្រព័ន្ធ +settings.general.header=ការ​កំណត់ > ទូទៅ +settings.logs.header=Settings > System Logs +logs.none=អ្នក​គ្មាន​កំណត់ហេតុ​ប្រព័ន្ធ​ទេ ។ +logs.content=សារ +logs.date=ម៉ោង +logs.filter.label=បង្ហាញ​កំណត់ហេតុ​សម្រាប់ +logs.filter.anytime=គ្រប់​ពេល +logs.filter.days.1=last 24 hours +logs.filter.days.3=last 3 days +logs.filter.days.7=last 7 days +logs.filter.days.14=last 14 days +logs.filter.days.28=last 28 days +logs.download.label=ទាញ​យក​កំណត់ហេតុ​ប្រព័ន្ធ +logs.download.buttontext=ទាញ​យក​កំណត់ហេតុ +logs.download.title=ទាញ​យក​កំណត់ហេតុ​ដើម្បី​ផ្ញើ +logs.download.continue=បន្ត +smartgroup.validation.prompt=សូម​បំពេញ​វាល​ដែល​ទាមទារ​ទាំងអស់ ។ អ្នក​អាច​បញ្ជាក់​​លក្ខខណ្ឌ​មួយ​ក្នុង​មួយ​វាល ។ +smartgroup.info=ដើម្បី​បង្កើត​ក្រុម​វៃ​ឆ្លាត, ជ្រើស​លក្ខខណ្ឌ​ដែល​អ្នក​ចង់បាន​​​ដើម្បី​ផ្គូផ្គង​ជាមួយ​ទំនាក់ទំនង​​សម្រាប់​ក្រុម​នេះ +smartgroup.contains.label=មាន +smartgroup.startswith.label=ចាប់ផ្ដើម​ដោយ +smartgroup.add.anotherrule=បន្ថែម​លក្ខខណ្ឌ​ផ្សេង​ទៀត +smartgroup.name.label=ឈ្មោះ +modem.port=ច្រក +modem.description=សេចក្ដី​ពណ៌នា +modem.locked=បាន​ចាក់សោ ? +traffic.header=ចរាចរ +traffic.update.chart=ធ្វើ​បច្ចុប្បន្នភាព​គំនូសតាង +traffic.filter.2weeks=បង្ហាញ​ពីរ​សប្ដាហ៍​ចុងក្រោយ +traffic.filter.between.dates=រវាង​កាលបរិច្ឆេទ +traffic.filter.reset=កំណត់​តម្រង​ឡើងវិញ +traffic.allgroups=បង្ហាញ​ក្រុម​ទាំងអស់ +traffic.all.folders.activities=បង្ហាញ​សកម្មភាព/ថត​ទាំងអស់ +traffic.sent=បាន​ផ្ញើ +traffic.received=បាន​ទទួល +traffic.total=សរុប +tab.message=សារ +tab.archive=ប័ណ្ណសារ +tab.contact=ទំនាក់ទំនង +tab.status=ស្ថានភាព +tab.search=ស្វែងរក +help.info=កំណែ​នេះ​គឺ​បេតា វា​គ្មាន​ជំនួយ​ជាប់​មក​ទេ ។ សូម​ចូល​ទៅ​វេទិកា​អ្នកប្រើ​ដើម្បី​មើល​ជំនួយ​អំពី​ចំណុច​នេះ +help.notfound=This help file is not yet available, sorry. +# IntelliSms Fconnection +intellisms.label=IntelliSMS +intellisms.type.label=ប្រភេទ +intellisms.name.label=ឈ្មោះ +intellisms.username.label=Username +intellisms.password.label=Password +intellisms.sendEnabled.label=Use for sending +intellisms.receiveEnabled.label=Use for receiving +intellisms.receiveProtocol.label=Protocol +intellisms.serverName.label=ឈ្មោះ​អ្នក​ផ្ញើ +intellisms.serverPort.label=Server Port +intellisms.emailUserName.label=Username +intellisms.emailPassword.label=Password +intellisms.description=Send and receive messages through an IntelliSMS connection. +intellisms.global.info=You will need to configure an account with IntelliSMS (www.intellisms.co.uk). +intelliSmsFconnection.send.validator.invalid=អ្នក​មិន​អាច​កំណត់​រចនាសម្ព័ន្ធ​ការ​តភ្ជាប់​ដោយ​គ្មាន​មុខងារ ផ្ញើ ឬ​បញ្ជូន​បាន​ទេ +intelliSmsFconnection.receive.validator.invalid=អ្នក​មិន​អាច​កំណត់​រចនាសម្ព័ន្ធ​ការ​តភ្ជាប់​ដោយ​គ្មាន​មុខងារ ផ្ញើ ឬ​បញ្ជូន​បាន​ទេ +#Controllers +contact.label=ទំនាក់ទំនង +contact.edited.by.another.user=មាន​អ្នកប្រើ​ផ្សេង​បាន​ធ្វើ​បច្ចុប្បន្នភាព​ទំនាក់ទំនង​នេះ ពេល​អ្នក​កំពុង​កែ +contact.exists.prompt=មាន​ទំនាក់ទំនង​បាន​ប្រើ​លេខ​នោះ​រួចហើយ +contact.exists.warn=ទំនាក់ទំនង​បាន​ប្រើ​លេខ​នេះ​មាន​រួចហើយ +contact.view.duplicate=មើល​ច្បាប់​ចម្លង +contact.addtogroup.error=អ្នក​មិន​អាច​បន្ថែម ឬ​លុប​ចេញពី​ក្រុម​ដូច​គ្នា​បាន​ទេ ! +contact.mobile.label=ទូរស័ព្ទ +fconnection.label=Fconnection +fconnection.name=Fconnection +fconnection.unknown.type=មិន​ស្គាល់​ប្រភេទ​ការ​តភ្ជាប់ ៖ +fconnection.test.message.sent=សាកល្បង​សារ​ដែល​នៅ​ក្នុង​ជួរ​ផ្ញើ ! +announcement.saved=បាន​រក្សា​ទុក​សេចក្ដី​ប្រកាស ហើយ​សារ​ត្រូវ​បាន​ដាក់​ក្នុង​ជួរ​ដើម្បី​ផ្ញើ +announcement.not.saved=មិន​អាច​រក្សាទុក​សេចក្ដី​ប្រកាស! +announcement.save.success=បាន​រក្សាទុក​សេចក្ដី​ប្រកាស {0} ! +announcement.id.exist.not=មិន​អាច​រក​ឃើញ​សេចក្ដី​ប្រកាស​ដែល​បាន​លេខ​សម្គាល់ {0} +autoreply.save.success=បាន​រក្សាទុក​ចម្លើយ​តប​ស្វ័យប្រវត្តិ {0} ! +autoreply.not.saved=មិន​អាច​រក្សាទុក​ចម្លើយ​តប​ស្វ័យប្រវត្តិ ! +report.creation.error=មាន​កំហុស​ក្នុង​បង្កើត​របាយការណ៍ +export.message.title=នាំចេញ​សារ FrontlineSMS +export.database.id=លេខ​សម្គាល់​មូលដ្ឋាន​ទិន្នន័យ +export.message.date.created=បាន​បង្កើត​កាលបរិច្ឆេទ +export.message.text=អត្ថបទ +export.message.destination.name=ឈ្មោះ​ទិសដៅ +export.message.destination.mobile=ទូរស័ព្ទ​ទិសដៅ +export.message.source.name=ឈ្មោះ​ប្រភព +export.message.source.mobile=ទូរស័ព្ទ​ប្រភព +export.contact.title=នាំចេញ​ទំនាក់ទំនង FrontlineSMS +export.contact.name=ឈ្មោះ +export.contact.mobile=ទូរស័ព្ទ +export.contact.email=អ៊ីមែល +export.contact.notes=ចំណាំ +export.contact.groups=ក្រុម +export.messages.name1={0} {1} (សារ{2}) +export.messages.name2={0} (សារ {1}) +export.contacts.name1={0} ក្រុម (ទំនាក់ទំនង {1}) +export.contacts.name2=ក្រុម​វៃ​ឆ្លាត{0} (ទំនាក់ទំនង{1}) +export.contacts.name3=ទំនាក់ទំនង​ទាំងអស់ (ទំនាក់ទំនង {0}) +folder.archived.successfully=បាន​ទុក​ថត​ក្នុង​ប័ណ្ណសារ​ដោយ​ជោគជ័យ ! +folder.unarchived.successfully=បាន​លុប​ថត​ពី​ក្នុង​ប័ណ្ណសារ​ដោយ​ជោគជ័យ ! +folder.trashed=ថត​ត្រូវ​បាន​ដាក់​ក្នុង​ធុងសំរាម ! +folder.restored=ថត​ត្រូវ​បាន​ស្ដារ ! +folder.exist.not=មិន​អាច​រក​ឃើញ​ដែល​មាន​លេខ​សម្គាល់ {0} +folder.renamed=បាន​ប្ដូរ​ឈ្មោះ​ថត +group.label=ក្រុម +group.name.label=ឈ្មោះ +group.update.success=បាន​ធ្វើ​បច្ចុប្បន្នភាព​ក្រុម​ដោយ​ជោគជ័យ +group.save.fail=ការ​រក្សាទុក​ក្រុម​បាន​បរាជ័យ +group.delete.fail=មិន​អាច​លុប​ក្រុម​បាន ។ កំពុង​ប្រើ​ដោយ​ការ​ជាវ +import.label=Import contacts and messages +import.backup.label=You can use the form below to import your contacts. You can also import messages that you have from a previous Frontline project. +import.prompt.type=Select the type of data you wish to import before uploading +import.messages=Messages in Frontline's CSV format +import.prompt=Select the file containing your data to initiate the import +import.upload.failed=បាន​​បរាជ័យ​ក្នុង​ការ​ផ្ទុក​ឯកសារ​ឡើង​ដោយសារ​ហេតុផល​មួយ​ចំនួន ។ +import.contact.save.error=បាន​ជួប​កំហុស​ពេល​រក្សាទុក​ទំនាក់ទំនង +import.contact.complete=បាន​នាំចូល​ទំនាក់ទំនង {0}; បាន​បរាជ័យ {1} +import.contact.exist=The imported contacts already exist. +import.contact.failed.label=Failed contact imports +import.contact.failed.info={0} contact(s) successfully imported.
{1} contact(s) could not be imported.
{2} +import.download.failed.contacts=Download a file containing the failed contacts. +import.message.save.error=បាន​ជួប​បញ្ហា​ពេល​រក្សាទុក​សារ +import.message.complete=បាន​នាំចូល​សារ {0} ; បាន​បរាជ័យ {1} +export.label=Export data from your Frontline workspace +export.backup.label=You can export your Frontline data as VCF/VCard, CSV or PDF +export.prompt.type=Select which data you wish to export +export.allcontacts=All of your contacts +export.inboxmessages=Your Inbox messages +export.submit.label=Export and download data +many.selected=បាន​ជ្រើស {0} {1}s +flash.message.activity.found.not=រក​មិន​ឃើញ​សកម្មភាព +flash.message.folder.found.not=រក​មិន​ឃើញ​ថត +flash.message=សារ +flash.message.fmessage=សារ {0} +flash.message.fmessages.many=សារ SMS {0} +flash.message.fmessages.many.one=សារ SMS 1 +fmessage.exist.not=មិន​អាច​រក​ឃើញ​សារ​ដែល​មាន​លេខ​សម្គាល់ {0} +flash.message.poll.queued=បាន​រក្សាទុក​ការ​ស្ទង់​មតិ ហើយ​សារ​ត្រូវ​បាន​ដាក់​ក្នុង​ជួរ​ដើម្បី​ផ្ញើ +flash.message.poll.not.saved=មិន​អាច​រក្សាទុក​ការ​ស្ទង់មតិ ! +system.notification.ok=យល់ព្រម +system.notification.fail=បរាជ័យ +flash.smartgroup.delete.unable=មិន​អាច​លុប​ក្រុម​វៃ​ឆ្លាត +flash.smartgroup.saved=បាន​រក្សាទុក​ក្រុម​វៃ​ឆ្លាត {0} +flash.smartgroup.save.failed=ការ​ក្សាទុក​ក្រុម​វៃ​ឆ្លាត​បាន​បរាជ័យ ។ កំហុស​គឺ {0} +smartgroup.id.exist.not=មិន​អាច​រក​ឃើញ​ក្រុម​វៃ​ឆ្លាត​ដែល​មាន​លេខ​សម្គាល់ {0} +smartgroup.save.failed=បាន​បរាជ័យ​ក្នុង​ការ​រក្សាទុក​ក្រុម​វៃ​ឆ្លាត{0} ដែល​មាន params {1}{2} កំហុស​គឺ ៖ {3} +searchdescriptor.searching=កំពុង​ស្វែងរក +searchdescriptor.all.messages=សារ​ទាំងអស់ +searchdescriptor.archived.messages=, រួម​ទាំង​សារ​ដែល​បាន​ទុក​ក្នុង​ប័ណ្ណសារ +searchdescriptor.exclude.archived.messages=, លើកលែងតែ​សារ​ដែល​បាន​ទុក​ក្នុង​ប័ណ្ណសារ +searchdescriptor.only=, តែ {0} ប៉ុណ្ណោះ +searchdescriptor.between=, រវាង {0} និង {1} +searchdescriptor.from=, ចាប់ពី {0} +searchdescriptor.until=, រហូត​ដល់ {0} +poll.title={0} +announcement.title={0} +autoreply.title={0} +folder.title={0} +frontlinesms.welcome=សូម​ស្វាគមន៍​មកកាន់​កម្មវិធី FrontlineSMS! \\o/ +failed.pending.fmessages=សារ​មិន​ទាន់​សម្រេច {0} បាន​បរាជ័យ ។ ចូល​ទៅកាន់​ប្រអប់​សារ​មិន​ទាន់​សម្រេច​ដើម្បី​មើល​វា ។ +subscription.title={0} +subscription.info.group=ក្រុម ៖ {0} +subscription.info.groupMemberCount=សមាជិក {0} +subscription.info.keyword=ពាក្យ​គន្លឹះ​កម្រិត​ខ្ពស់ ៖ {0} +subscription.sorting.disable=បិទ​ការ​តម្រៀប​ស្វ័យប្រវត្តិ +subscription.info.joinKeywords=ចូលរួម ៖ {0} +subscription.info.leaveKeywords=ចេញ ៖ {0} +subscription.group.goto=មើល​ក្រុម +subscription.group.required.error=ការ​ជាវ​ត្រូវតែ​មាន​ក្រុម +subscription.save.success=បាន​រក្សាទុក​ការ​ជាវ {0} ! +language.label=ភាសា +language.prompt=ប្ដូរ​ភាសា​ចំណុចប្រទាក់​របស់​កម្មវិធី FrontlineSMS +frontlinesms.user.support=ជំនួយ​អ្នកប្រើ​កម្មវិធី FrontlineSMS +download.logs.info1=ព្រមាន ៖ ក្រុម FrontlineSMS មិន​អាច​ឆ្លើយតប​ដោយ​ផ្ទាល់​ទៅកាន់​កំណត់ហេតុ​ដែល​ដាក់​ស្នើ​បាន​ទេ ។ ប្រសិនបើ​អ្នក​មាន​សំណើ​គាំទ្រ​អ្នកប្រើ, សូម​ពិនិត្យ​ឯកសារ​ជំនួយ​ដើម្បី​មើល​ថា​តើ​អ្នក​អាច​រក​ចម្លើយ​នៅ​​ទីនេះ​បាន ។ បើ​មិន​មាន​ទេ សូម​រាយការណ៍​បញ្ហា​របស់​អ្នក​តាម​វេទិកា​ជំនួយ​អ្នកប្រើ ៖ +download.logs.info2=អ្នកប្រើ​ផ្សេង​ក៏​អាច​ផ្ញើ​របាយការណ៍​ដូចគ្នា​ដើម្បី​បាន​ដំណោះស្រាយ​ផង​ដែរ ! ដើម្បី​បន្ត និង​ដាក់​ស្នើ​កំណត់ហេតុ​របស់​​​​​អ្នក, សូម​ចុច 'បន្ត' +# Configuration location info +configuration.location.title=កំណត់​រចនាសម្ព័ន្ធ​ទីតាំង +configuration.location.description=These files include your database and other settings, which you may wish to back up elsewhere. +configuration.location.instructions=អ្នក​អាច​រក​ឃើញ​កម្មវិធី​កំណត់​រចនាសម្ព័ន្ធ​របស់​អ្នក​នៅ {1} ។ ឯកសារ​ទាំងនេះ​រួម​មាន​មូលដ្ឋាន​ទិន្នន័យ និង​ការ​កំណត់​ផ្សេងទៀត​របស់​អ្នក ដែល​អ្នក​អាច​បម្រុងទុក​នៅ​កន្លែង​ផ្សេង​បាន ។ +dynamicfield.contact_name.label=ឈ្មោះ​ទំនាក់ទំនង +dynamicfield.contact_number.label=លេខ​ទំនាក់ទំនង +dynamicfield.keyword.label=ពាក្យ​គន្លឹះ +dynamicfield.message_content.label=មាតិកា​សារ +# TextMessage domain +fmessage.queued=បាន​ដាក់​សារ​ក្នុង​ជួរ​ដើម្បី​ផ្ញើ​ទៅ {0} +fmessage.queued.multiple=បាន​ដាក់​សារ​ក្នុង​ជួរ​ដើម្បី​ផ្ញើ​ទៅ​អ្នក​ទទួល {0} +fmessage.retry.success=បាន​ដាក់​សារ​ក្នុង​ជួរ​ដើម្បី​ផ្ញើ​ទៅ {0} +fmessage.retry.success.multiple=បាន​ដាក់​សារ {0} ក្នុង​ជួរ​ដើម្បី​ផ្ញើ +fmessage.displayName.label=ឈ្មោះ +fmessage.text.label=សារ +fmessage.date.label=កាលបរិច្ឆេទ +fmessage.to=ជូន​ចំពោះ ៖ {0} +fmessage.to.multiple=ជូន​ចំពោះ ៖ អ្នក​ទទួល {0} +fmessage.quickmessage=Send message +fmessage.archive=ប័ណ្ណសារ +fmessage.activity.archive=ទុក​ក្នុង​ប័ណ្ណសារ {0} +fmessage.unarchive=មិន​ទុក​ក្នុង​ប័ណ្ណសារ +fmessage.export=នាំចេញ +fmessage.rename=ប្ដូរ​ឈ្មោះ {0} +fmessage.edit=កែសម្រួល {0} +fmessage.delete=Delete +fmessage.moreactions=សកម្មភាព​ច្រើន​ទៀត... +fmessage.footer.show=បង្ហាញ +fmessage.footer.show.failed=បាន​បរាជ័យ +fmessage.footer.show.all=ទាំងអស់ +fmessage.footer.show.starred=បាន​ដាក់​ផ្កាយ +fmessage.footer.show.incoming=ចូល +fmessage.footer.show.outgoing=ចេញ +fmessage.archive.back=ថយក្រោយ +fmessage.activity.sentmessage=(បាន​ផ្ញើ​សារ {0}) +fmessage.failed=បាន​បរាជ័យ +fmessage.header=សារ +fmessage.section.inbox=ប្រអប់​ទទួល +fmessage.section.sent=បាន​ផ្ញើ +fmessage.section.pending=មិនទាន់​សម្រេច +fmessage.section.trash=ធុងសំរាម +fmessage.addsender=បន្ថែម​ទៅ​ទំនាក់ទំនង +fmessage.resend=ផ្ញើ​ឡើងវិញ +fmessage.retry=ព្យាយាម​ម្ដងទៀត +fmessage.reply=ឆ្លើយតប +fmessage.forward=បញ្ជូន​បន្ត +fmessage.messages.none=គ្មាន​សារ​នៅ​ទីនេះ​ទេ ! +fmessage.selected.none=មិន​បាន​ជ្រើស​សារ +fmessage.move.to.header=ផ្លាស់ទី​សារ​ទៅកាន់... +fmessage.move.to.inbox=ប្រអប់​ទទួល +fmessage.archive.many=Archive selected +fmessage.count=សារ 1 +fmessage.count.many=សារ {0} +fmessage.many=សារ +fmessage.delete.many=Delete selected +fmessage.reply.many=Reply selected +fmessage.restore=ស្ដារ +fmessage.restore.many=ស្ដារ +fmessage.retry.many=Retry selected +fmessage.selected.many=បាន​ជ្រើស​សារ {0} +fmessage.unarchive.many=Unarchive selected +# TODO move to poll.* +fmessage.showpolldetails=បង្ហាញ​ក្រាហ្វិក +fmessage.hidepolldetails=លាក់​ក្រាហ្វិក +# TODO move to search.* +fmessage.search.none=រក​មិន​ឃើញ​សារ +fmessage.search.description=ចាប់ផ្ដើម​ការ​ស្វែងរក​ថ្មី​នៅ​ខាងឆ្វេង +fmessage.connection.receivedon=បាន​ទទួល​នៅ ៖ +activity.name=ឈ្មោះ +activity.delete.prompt=ផ្លាស់ទី {0} ទៅ​ធុងសំរាម​។ វា​នឹង​ផ្ទេរ​សារ​ដែល​ពាក់ព័ន្ធ​ទាំងអស់​ទៅ​ក្នុង​ធុងសំរាម ។ +activity.label=សកម្មភាព +activity.categorize=ការ​បែងចែក​ចម្លើយ​តប +magicwand.title=បន្ថែម​កន្សោម​ជំនួស +folder.create.success=បាន​បង្កើត​ថត​ដោយ​ជោគជ័យ +folder.create.failed=មិន​អាច​បង្កើត​ថត +folder.name.validator.error=ឈ្មោះ​ថត​បាន​ប្រើ​រួចហើយ +folder.name.blank.error=ឈ្មោះ​ថត​មិន​អាច​ទទេ​បាន​ឡើយ +poll.name.blank.error=ឈ្មោះ​ការ​ស្ទង់​មតិ​មិន​អាច​ទទេ​បាន​ឡើយ +poll.name.validator.error=ឈ្មោះ​ការ​ស្ទង់​មតិ​បាន​ប្រើ​រួចហើយ +autoreply.name.blank.error=ឈ្មោះ​ចម្លើយ​តប​ស្វ័យប្រវត្តិ​មិន​អាច​ទទេ​បាន​ឡើយ +autoreply.name.validator.error=ឈ្មោះ​ចម្លើយ​តប​ស្វ័យប្រវត្តិ​បាន​ប្រើ​រួចហើយ +announcement.name.blank.error=ឈ្មោះ​សេចក្ដី​ប្រកាស​មិន​អាច​ទទេ​បាន​ឡើយ +announcement.name.validator.error=ឈ្មោះ​សេចក្ដី​ប្រកាស​បាន​ប្រើ​រួចហើយ +group.name.blank.error=ឈ្មោះ​ក្រុម​មិន​អាច​ទទេ​បាន​ឡើយ +group.name.validator.error=ឈ្មោះ​ក្រុម​បាន​ប្រើ​រួចហើយ +#Jquery Validation messages +jquery.validation.required=វាល​នេះ​ត្រូវ​បាន​ទាមទារ ។ +jquery.validation.remote=សូម​កែ​វាល​នេះ ។ +jquery.validation.email=សូម​បញ្ចូល​អាសយដ្ឋាន​អ៊ីមែល​ត្រឹមត្រូវ ។ +jquery.validation.url=សូម​បញ្ចូល URL ត្រឹមត្រូវ ។ +jquery.validation.date=សូម​បញ្ចូល​កាលបរិច្ឆេទ​ត្រឹមត្រូវ ។ +jquery.validation.dateISO=សូម​បញ្ចូល​កាលបរិច្ឆេទ​ត្រឹមត្រូវ (ISO) ។ +jquery.validation.number=សូម​បញ្ចូល​លេខ​ត្រឹមត្រូវ ។ +jquery.validation.digits=សូម​បញ្ចូល​តែ​តួលេខ​ប៉ុណ្ណោះ ។ +jquery.validation.creditcard=សូម​បញ្ចូល​លេខ​ប័ណ្ណ​ឥណទាន​ត្រឹមត្រូវ ។ +jquery.validation.equalto=សូម​បញ្ចូល​តម្លៃ​ដូចគ្នា​ម្ដងទៀត ។ +jquery.validation.accept=សូម​បញ្ចូល​តម្លៃ​ដែល​មាន​ផ្នែក​បន្ថែម​ត្រឹមត្រូវ ។ +jquery.validation.maxlength=សូម​បញ្ចូល​កុំឲ្យ​ច្រើន​ជាង {0} តួអក្សរ ។ +jquery.validation.minlength=សូម​បញ្ចូល​យ៉ាង​ហោច {0} តួអក្សរ ។ +jquery.validation.rangelength=សូម​បញ្ចូល​តម្លៃ​តួអក្សរ​ដែល​មាន​ប្រវែង​ពី {0} ទៅ {1} ។ +jquery.validation.range=សូម​បញ្ចូល​តម្លៃ​ចន្លោះ​ពី {0} ទៅ {1} ។ +jquery.validation.max=សូម​បញ្ចូល​តម្លៃ​ដែល​តូច​ជាង ឬ​ស្មើនឹង {0} ។ +jquery.validation.min=សូម​បញ្ចូល​តម្លៃ​ដែល​ធំជាង ឬ​ស្មើនឹង {0} ។ +# Webconnection common +webconnection.select.type=ជ្រើស​សេវាកម្ម​បណ្ដាញ ឬ​កម្មវិធី​ដើម្បី​តភ្ជាប់​ទៅ ៖ +webconnection.type=ជ្រើស​ប្រភេទ +webconnection.title={0} +webconnection.label=ការ​តភ្ជាប់​បណ្ដាញ +webconnection.description=តភ្ជាប់​ទៅ​សេវាកម្ម​បណ្ដាញ ។ +webconnection.sorting=តម្រៀប​ស្វ័យប្រវត្តិ +webconnection.configure=កំណត់​រចនាសម្ព័ន្ធ​សេវាកម្ម +webconnection.api=បង្ហាញ API +webconnection.api.info=FrontlineSMS អាច​ត្រូវ​បាន​កំណត់​រចនាសម្ព័ន្ធ​ដើម្បី​ទទួល​សំណើ​ចូល​ពី​សេវាកម្ម​ខាងក្រៅ និង​បិទ/បើក​សារ​ចេញ ។ សម្រាប់​ព័ត៌មាន​លម្អិត សូម​មើល​ផ្នែក​ជំនួយ​របស់​ការ​​​​តភ្ជាប់​បណ្ដាញ ។ +webconnection.api.enable.label=បើក API +webconnection.api.secret.label=សោ​សម្ងាត់​របស់ API ៖ +webconnection.api.disabled=បាន​បិទ API +webconnection.api.url=API URL +webconnection.moreactions.retryFailed=retry failed uploads +webconnection.failed.retried=Failed web connections have been scheduled for resending. +webconnection.url.error.locahost.invalid.use.ip=Please use 127.0.0.1 instead of "locahost" for localhost urls +webconnection.url.error.url.start.with.http=Invalid URL (should start with http:// or https://) +# Webconnection - generic +webconnection.generic.label=សេវាកម្ម​បណ្ដាញ​ផ្សេងទៀត +webconnection.generic.description=ផ្ញើ​សារ​ទៅ​សេវាកម្ម​បណ្ដាញ​ផ្សេងទៀត +webconnection.generic.subtitle=ការ​តភ្ជាប់​បណ្ដាញ HTTP +# Webconnection - Ushahidi/Crowdmap +webconnection.ushahidi.label=Crowdmap / Ushahidi +webconnection.ushahidi.description=ផ្ញើ​សារ​ទៅ​ម៉ាស៊ីន​មេ​របស់ CrowdMap ឬ Ushahidi ។ +webconnection.ushahidi.key.description=សោ API សម្រាប់ Crowdmap ឬ Ushahidi អាច​រក​ឃើញ​នៅ​ក្នុង​ការ​កំណត់​តំបន់​បណ្ដាញ​របស់ Crowdmap ឬ Ushahidi ។ +webconnection.ushahidi.url.label=អាសយដ្ឋាន ៖ +webconnection.ushahidi.key.label=សោ API របស់ Ushahidi ៖ +webconnection.crowdmap.url.label=អាសយដ្ឋាន​ប្រើប្រាស់​របស់ Crowdmap ៖ +webconnection.crowdmap.key.label=សោ API របស់ Crowdmap ៖ +webconnection.ushahidi.serviceType.label=ជ្រើស​សេវាកម្ម +webconnection.ushahidi.serviceType.crowdmap=Crowdmap +webconnection.ushahidi.serviceType.ushahidi=Ushahidi +webconnection.crowdmap.url.suffix.label=.crowdmap.com +webconnection.ushahidi.subtitle=ការ​តភ្ជាប់​បណ្ដាញ​ទៅ {0} +webconnection.ushahidi.service.label=សេវាកម្ម ៖ +webconnection.ushahidi.fsmskey.label=FrontlineSMS API សម្ងាត់ ៖ +webconnection.ushahidi.crowdmapkey.label=សោ API របស់ Crowdmap/Ushahidi ៖ +webconnection.ushahidi.keyword.label=ពាក្យ​គន្លឹះ ៖ +url.invalid.url=The URL provided is invalid. +webconnection.confirm=បញ្ជាក់ +webconnection.keyword.title=ផ្ទេរ​រាល់​សារ​ចូល​ដែល​មាន​ពាក្យ​គន្លឹះ​ដូច​ខាងក្រោម ៖ +webconnection.all.messages=កុំ​ប្រើ​ពាក្យ​គន្លឹះ (សារ​ចូល​ទាំងអស់​នឹង​ត្រូវ​បាន​បញ្ជូន​បន្ត​ទៅកាន់​ការ​តភ្ជាប់​បណ្ដាញ​នេះ +webconnection.httpMethod.label=ជ្រើស​វិធីសាស្ត្រ HTTP ៖ +webconnection.httpMethod.get=GET +webconnection.httpMethod.post=POST +webconnection.name.prompt=ដាក់ឈ្មោះ​ការ​តភ្ជាប់​បណ្ដាញ​នេះ +webconnection.details.label=ព័ត៌លម្អិត​នៃ​ការ​បញ្ជាក់ +webconnection.parameters=កំណត់​រចនាសម្ព័ន្ធ​ព័ត៌មាន​ដែល​បាន​ផ្ញើ​ទៅកាន់​ម៉ាស៊ីន​មេ +webconnection.parameters.confirm=បាន​កំណត់​រចនាសម្ព័ន្ធ​ព័ត៌មាន​ដែល​ផ្ញើ​ទៅកាន់​ម៉ាស៊ីន​មេ +webconnection.keyword.label=ពាក្យ​គន្លឹះ +webconnection.none.label=គ្មាន +webconnection.url.label=Url ម៉ាស៊ីន​មេ ៖ +webconnection.param.name=ឈ្មោះ ៖ +webconnection.param.value=តម្លៃ ៖ +webconnection.add.anotherparam=បន្ថែម​ប៉ារ៉ាម៉ែត្រ +dynamicfield.message_body.label=អត្ថបទ​សារ +dynamicfield.message_body_with_keyword.label=អត្ថបទ​សារ​ដែល​មាន​ពាក្យ​គន្លឹះ +dynamicfield.message_src_number.label=លេខ​ទំនាក់ទំនង +dynamicfield.message_src_name.label=ឈ្មោះ​ទំនាក់ទំនង +dynamicfield.message_timestamp.label=ត្រា​ពេលវេលា​សារ +webconnection.keyword.validation.error=បាន​ទាមទារ​ពាក្យ​គន្លឹះ +webconnection.url.validation.error=បាន​ទាមទារ Url +webconnection.save=បាន​រក្សាទុក​ការ​តភ្ជាប់​បណ្ដាញ ! +webconnection.saved=បាន​រក្សាទុក​ការ​តភ្ជាប់​បណ្ដាញ ! +webconnection.save.success=បាន​រក្សាទុក​ការ​តភ្ជាប់​បណ្ដាញ ! {0} +webconnection.generic.service.label=សេវាកម្ម ៖ +webconnection.generic.httpMethod.label=វិធីសាស្ត្រ Http ៖ +webconnection.generic.url.label=អាសយដ្ឋាន ៖ +webconnection.generic.parameters.label=បាន​កំណត់​រចនាសម្ព័ន្ធ​ព័ត៌មាន​ដែល​ផ្ញើ​ទៅ​ម៉ាស៊ីន​មេ ៖ +webconnection.generic.keyword.label=ពាក្យ​គន្លឹះ +webconnection.generic.key.label=សោ API ៖ +frontlinesms2.Keyword.value.validator.error.frontlinesms2.UshahidiWebconnection.keyword.value=តម្លៃ​ពាក្យ​គន្លឹះ​មិន​ត្រឹមត្រូវ +#Subscription i18n +subscription.label=ការ​ជាវ +subscription.name.prompt=ដាក់​ឈ្មោះ​ការ​ជាវ​នេះ +subscription.details.label=ការ​បញ្ជាក់​លម្អិត +subscription.description=អនុញ្ញាត​ឲ្យ​មនុស្ស​ចូលរួម និង​ចេញពី​ក្រុម​ដោយ​ស្វ័យប្រវត្តិ​ដោយ​ប្រើ​ពាក្យ​គន្លឹះ​សារ +subscription.select.group=ជ្រើស​ក្រុម​សម្រាប់​ការ​ជាវ +subscription.group.none.selected=ជ្រើស​ក្រុម +subscription.autoreplies=ចម្លើយ​តប​ស្វ័យប្រវត្តិ +subscription.sorting=ការ​តម្រៀប​ស្វ័យប្រវត្តិ +subscription.sorting.header=ដំណើរការ​សារ​ស្វ័យប្រវត្តិ​ដោយ​ប្រើ​ពាក្យ​គន្លឹះ (ជា​ជម្រើស) +subscription.confirm=បញ្ជាក់ +subscription.group.header=ជ្រើស​ក្រុម +subscription.group.description=ទំនាក់ទំនង​អាច​ត្រូវ​បាន​បន្ថែម និង​លុបចេញ​ពី​ក្រុម​ដោយ​ស្វ័យប្រវត្តិ នៅ​ពេល FrontlineSMS ទទួល​សារ​ដែល​មាន​ពាក្យ​គន្លឹះ​​ពិសេស ។ +subscription.keyword.header=បញ្ចូល​ពាក្យ​គន្លឹះ​សម្រាប់​ការ​ជាវ​នេះ +subscription.top.keyword.description=បញ្ចូល​ពាក្យ​គន្លឹះ​កម្រិត​ខ្ពស់​ដែល​អ្នកប្រើ​នឹង​ប្រើ​ដើម្បី​ជ្រើស​ក្រុម​នេះ ។ +subscription.top.keyword.more.description=អ្នក​អាច​បញ្ចូល​ពាក្យ​គន្លឹះ​​​​កម្រិត​ខ្ពស់​ច្រើន​បាន​សម្រាប់​ជម្រើស​នីមួយៗ​ និងបំបែក​ដោយ​សញ្ញា​ក្បៀស ។ ពាក្យ​គន្លឹះ​កម្រិត​ខ្ពស់​ចាំបាច់​ត្រូវ​មានតែ​មួយ​សម្រាប់​គ្រប់​សកម្មភាព​ទាំងអស់ ។ +subscription.keywords.header=បញ្ចូល​ពាក្យ​គន្លឹះ​សម្រាប់​ចូលរួម និង​ចេញពី​ក្រុម​នេះ ។ +subscription.keywords.description=អ្នក​អាច​បញ្ចូល​ពាក្យ​គន្លឹះ​ច្រើន​ដែល​បំបែក​ដោយ​សញ្ញា​ក្បៀស ។ ប្រសិនបើ​បាន​ពាក្យ​គន្លឹះ​កម្រិត​ខ្ពស់​នៅ​ខាង​លើ នោះ​ពាក្យ​គន្លឹះ​ចូលរួម និង​ពាក្យ​គន្លឹះ​ចេញ​នេះ​ចាំបាច់​ត្រូវ​មានតែ​មួយ​សម្រាប់​គ្រប់​​សកម្មភាព​ទាំងអស់ ។ +subscription.default.action.header=ជ្រើស​សកម្មភាព​មួយ​ពេល​មិន​បាន​ផ្ញើ​ពាក្យ​គន្លឹះ +subscription.default.action.description=ជ្រើស​សកម្មភាព​ដែល​ចង់បាន ពេល​សារ​ផ្គូផ្គង​ជាមួយ​ពាក្យ​​​​គន្លឹះ​កម្រិត​ខ្បស់ ប៉ុន្តែ​មិន​មាន​ពាក្យ​គន្លឹះ​​ចូលរួម ឬ​ចេញ ៖ +subscription.keywords.leave=ពាក្យ​គន្លឹះ​ចេញ +subscription.keywords.join=ពាក្យ​គន្លឹះ​ចូលរួម +subscription.default.action.join=បន្ថែម​ទំនាក់ទំនង​ទៅ​ក្រុម +subscription.default.action.leave=លុប​ទំនាក់ទំនង​ចេញពី​ក្រុម +subscription.default.action.toggle=បិទ/បើក​សមាជិកភាព​ក្រុម​របស់​ទំនាក់ទំនង +subscription.autoreply.join=ផ្ញើ​ចម្លើយ​តប​ស្វ័យប្រវត្តិ ពេល​ទំនាក់ទំនង​ចូលរួម​ក្រុម +subscription.autoreply.leave=ផ្ញើ​ចម្លើយ​តប​ស្វ័យប្រវត្តិ ពេល​ទំនាក់ទំនង​ចេញពី​ក្រុម +subscription.confirm.group=ក្រុម +subscription.confirm.keyword=ពាក្យ​គន្លឹះ +subscription.confirm.join.alias=ពាក្យ​គន្លឹះ​ចូលរួម +subscription.confirm.leave.alias=ពាក្យ​គន្លឹះ​ចេញ +subscription.confirm.default.action=សកម្មភាព​លំនាំដើម +subscription.confirm.join.autoreply=ចម្លើយ​តប​ស្វ័យប្រវត្តិ​ចូលរួម +subscription.confirm.leave.autoreply=ចម្លើយ​តប​ស្វ័យប្រវត្តិ​ចេញ +subscription.info1=បាន​​រក្សាទុក​ការ​ជាវ ហើយ​ឥឡូវ​គឺ​សកម្ម +subscription.info2=សារ​ចូល​ដែល​ផ្គូផ្គង​ជាមួយ​ពាក្យ​គន្លឹះ​នេះ នឹង​ប្ដូរ​សមាជិកភាព​ក្រុម​របស់​ទំនាក់ទំនង​ដូច​បាន​កំណត់ +subscription.info3=ដើម្បី​មើល​ការ​ជាវ, ចុច​លើ​វា​ក្នុង​ម៉ឺនុយ​ខាងឆ្វេង​ដៃ +subscription.categorise.title=ការ​បែងចែក​សារ +subscription.categorise.info=សូម​ជ្រើស​សកម្មភាព​ដើម្បី​អនុវត្ត​ជាមួយ​អ្នក​ផ្ញើ​​នៃ​សារ​ដែល​​បាន​ជ្រើស ពេល​ពួកគេ​ត្រូវ​បាន​បន្ថែម​ទៅកាន់ {0} +subscription.categorise.join.label=បន្ថែម​អ្នក​ផ្ញើ​ទៅកាន់ {0} +subscription.categorise.leave.label=លុប​អ្នក​ផ្ញើ​ចេញពី {0} +subscription.categorise.toggle.label=បិទ/បើក​អ្នក​ផ្ញើ' សមាជិកភាព​នៃ {0} +subscription.join=ចូលរួម +subscription.leave=ចេញ +subscription.sorting.example.toplevel=ឧ. ដំណោះស្រាយ +subscription.sorting.example.join=ឧ. ជាវ, ចូលរួម +subscription.sorting.example.leave=ឧ. ឈប់​ជាវ, ចេញ +subscription.keyword.required=បាន​ទាមទារ​ពាក្យ​គន្លឹះ +subscription.jointext.required=សូម​បញ្ចូល​អត្ថបទ​ការ​ឆ្លើយតប​ស្វ័យប្រវត្តិ​ចូលរួម +subscription.leavetext.required=សូម​បញ្ចូល​អត្ថបទ​ការ​ឆ្លើយតប​ស្វ័យប្រវត្តិ​ចេញ +subscription.moreactions.delete=លុប​ការ​ជាវ +subscription.moreactions.rename=ប្ដូរ​ឈ្មោះ​ការ​ជាវ +subscription.moreactions.edit=កែសម្រួល​ការ​ជាវ +subscription.moreactions.export=នាំចេញ​ការ​ជាវ +# Generic activity sorting +activity.generic.sorting=ដំណើរការ​ស្វ័យប្រវត្តិ +activity.generic.sorting.subtitle=ដំណើរការ​សារ​ស្វ័យប្រវត្តិ​ដោយ​ប្រើ​ពាក្យ​គន្លឹះ (ជា​ជម្រើស) +activity.generic.sort.header=ដំណើរការ​សារ​ស្វ័យប្រវត្តិ​ដោយ​ប្រើ​ពាក្យ​គន្លឹះ (ជា​ជម្រើស) +activity.generic.sort.description=ប្រសិនបើ​មនុស្ស​ផ្ញើ​សារ​ចូល​ដោយ​ចាប់ផ្ដើម​ជាមួយ​ពាក្យ​គន្លឹះ​ជាក់លាក់, FrontlineSMS អាច​ដំណើរការ​សារ​នៅ​លើ​ប្រព័ន្ធ​របស់​អ្នក​បាន ។ +activity.generic.keywords.title=បញ្ចូល​ពាក្យ​គន្លឹះ​សម្រាប់​សកម្មភាព ។ អ្នក​អាច​បញ្ចូល​ពាក្យ​គន្លឹះ​ច្រើ​បាន​ដែល​បំបែក​ដោយ​សញ្ញា​ក្បៀស ៖ +activity.generic.keywords.subtitle=បញ្ចូល​ពាក្យ​គន្លឹះ​សម្រាប់​សកម្មភាព +activity.generic.keywords.info=អ្នក​អាច​បញ្ចូល​ពាក្យ​គន្លឹះ​ច្រើ​បាន​ដែល​បំបែក​ដោយ​សញ្ញា​ក្បៀស ៖ +activity.generic.no.keywords.title=កុំ​ប្រើ​ពាក្យ​គន្លឹះ +activity.generic.no.keywords.description=សារ​ចូល​ទាំងអស់​ដែល​មិន​ផ្គូផ្គង​ជាមួយ​ពាក្យ​គន្លឹះ​នឹង​បិទ/បើក​សកម្មភាព​នេះ +activity.generic.disable.sorting=កុំ​តម្រៀប​សារ​ដោយ​ស្វ័យប្រវត្តិ +activity.generic.disable.sorting.description=សារ​នឹង​មិន​ដំណើរការ​ដោយ​ស្វ័យប្រវត្តិ​ជាមួយ​សកម្មភាព​នេះ​ទេ +activity.generic.enable.sorting=ដំណើរការ​ចម្លើយ​តប​មាន​ពាក្យ​គន្លឹះ​ស្វ័យប្រវត្តិ +activity.generic.sort.validation.unique.error=ពាក្យ​គន្លឹះ​ត្រូវតែ​មានតែ​មួយ +activity.generic.keyword.in.use=ពាក្យ​គន្លឹះ {0} ត្រូវ​បាន​ប្រើ​រួចហើយ​ដោយ​សកម្មភាព {1} +activity.generic.global.keyword.in.use=សកម្មភាព {0} ត្រូវ​បាន​កំណត់​ដើម្បី​ទទួល​សារ​​​​​ដែល​មិន​ផ្គូផ្គង​ជាមួយ​ពាក្យ​គន្លឹះ​ផ្សេង ។ អ្នក​អាច​មាន​សកម្មភាព​សកម្ម​តែមួយ​ប៉ុណ្ណោះ​ជាមួយ​ការ​កំណត់​នេះ +#basic authentication +auth.basic.label=Basic Authentication +auth.basic.info=Require a username and password for accessing FrontlineSMS across the network +auth.basic.enabled.label=Enable Basic Authentication +auth.basic.username.label=Username +auth.basic.password.label=Password +auth.basic.confirmPassword.label=Confirm Password +auth.basic.password.mismatch=Passwords don't match +newfeatures.popup.title=លក្ខណៈ​ថ្មី +newfeatures.popup.showinfuture=បង្ហាញ​ប្រអប់​នេះ​នៅ​ពេល​ខាងមុខ +dynamicfield.message_text.label=អត្ថបទ​សារ +dynamicfield.message_text_with_keyword.label=អត្ថបទ​សារ​ដែល​មាន​ពាក្យ​គន្លឹះ +dynamicfield.sender_name.label=ឈ្មោះ​អ្នក​ផ្ញើ +dynamicfield.sender_number.label=លេខ​អ្នក​ផ្ញើ +dynamicfield.recipient_number.label=លេខ​អ្នក​ទទួល +dynamicfield.recipient_name.label=ឈ្មោះ​អ្នក​ទទួល +# Smpp Fconnection +smpp.label=SMPP Account +smpp.type.label=ប្រភេទ +smpp.name.label=ឈ្មោះ +smpp.send.label=Use for sending +smpp.receive.label=Use for receiving +smpp.url.label=SMSC URL +smpp.port.label=SMSC Port +smpp.username.label=Username +smpp.password.label=Password +smpp.fromNumber.label=From number +smpp.description=Send and receive messages through an SMSC +smpp.global.info=You will need to get an account with your phone network of choice. +smpp.send.validator.invalid=You cannot configure a connection without SEND or RECEIVE fuctionality. +routing.title=Create rules for which phone number is used by outgoing messages. +routing.info=These rules will determine how the system selects which connection or phone number to use to send outgoing messages. Remember, the phone number seen by recipients may depend on the rules you set here. Also, changing this configuration may affect the cost of sending messages. +routing.rules.sending=When sending outgoing messages: +routing.rules.not_selected=If none of the above rules match: +routing.rules.otherwise=Otherwise: +routing.rules.device=Use {0} +routing.rule.uselastreceiver=Send through most recent number that the contact messaged +routing.rule.useany=Use any available connection's phone number +routing.rule.dontsend=Do not send the message +routing.notification.no-available-route=Outgoing message(s) not sent due to your routing preferences. +routing.rules.none-selected.warning=Warning: You have no rules or phone numbers selected. No messages will be sent. If you wish to send messages, please enable a connection. +customactivity.overview=Overview +customactivity.title={0} +customactivity.confirm=បញ្ជាក់ +customactivity.label=Custom Activity Builder +customactivity.description=Create your own activity from scratch by applying a custom set of actions to your specified keyword +customactivity.name.prompt=Name this activity +customactivity.moreactions.delete=Delete activity +customactivity.moreactions.rename=Rename activity +customactivity.moreactions.edit=Edit activity +customactivity.moreactions.export=Export activity +customactivity.text.none=គ្មាន +customactivity.config=Configure +customactivity.config.description=Build and configure a set of actions for this activity. The actions will all be executed when a message matches the criteria you set on the previous step. +customactivity.info=Your Custom Activity has been created, and any messages containing your keyword will have the specified actions applied to it. +customactivity.info.warning=Without a keyword, all incoming messages will trigger the actions in this Custom Activity. +customactivity.info.note=Note: If you archive the Custom Activity, incoming messages will no longer be sorted for it. +customactivity.save.success={0} activity saved +customactivity.action.steps.label=Action Steps +validation.group.notnull=Please select a group +customactivity.join.description=Joining "{0}" group +customactivity.leave.description=Leaving "{0}" group +customactivity.forward.description=Forwarding with "{0}" +customactivity.webconnectionStep.description=Upload to "{0}" +customactivity.reply.description=Reply with "{0}" +customactivity.step.join.add=Add sender to group +customactivity.step.join.title=Add sender to group* +customactivity.step.leave.add=លុប​អ្នក​ផ្ញើ​ចេញពី {0} +customactivity.step.leave.title=Remove sender from group* +customactivity.step.reply.add=Send Autoreply +customactivity.step.reply.title=Enter message to autoreply to sender* +customactivity.step.forward.add=Forward message +customactivity.step.forward.title=Automatically forward a message to one or more contacts +customactivity.manual.sorting=Automatic processing disabled +customactivity.step.webconnectionStep.add=Upload message to a URL +customactivity.step.webconnectionStep.title=Upload message to a URL +customactivity.validation.error.autoreplytext=Reply message is required +customactivity.validation.error.name=បាន​ទាមទារ Url +customactivity.validation.error.url=បាន​ទាមទារ Url +customactivity.validation.error.paramname=Parameter name is required +recipientSelector.keepTyping=Keep typing... +recipientSelector.searching=កំពុង​ស្វែងរក +validation.recipients.notnull=Please select at least one recipient +localhost.ip.placeholder=your-ip-address diff --git a/plugins/frontlinesms-core/grails-app/i18n/messages_nl.properties b/plugins/frontlinesms-core/grails-app/i18n/messages_nl.properties new file mode 100644 index 000000000..899520bc3 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/i18n/messages_nl.properties @@ -0,0 +1,1065 @@ +# FrontlineSMS English translation by the FrontlineSMS team, Nairobi +language.name=Nederlands +# General info +app.version.label=Versie +# Common action imperatives - to be used for button labels and similar +action.ok=OK +action.close=Sluiten +action.cancel=Annuleren +action.done=Uitgevoerd +action.next=Volgende +action.prev=Vorige +action.back=Terug +action.create=Aanmaken +action.edit=Bewerken +action.rename=Nieuwe naam geven +action.save=Opslaan +action.save.all=Save Selected +action.delete=Verwijderen +action.delete.all=Delete Selected +action.send=Verzenden +action.export=Exporteren +action.view=Tonen +content.loading=Bezig met laden... +# Messages when FrontlineSMS server connection is lost +server.connection.fail.title=De verbinding met de server is mislukt. +server.connection.fail.info=FrontlineSMS opnieuw opstarten, of dit venster sluiten. +#Connections: +connection.creation.failed=De verbinding kon niet tot stand worden gebracht {0} +connection.route.disabled=Route is vernietigd van {0} tot {1} +connection.route.successNotification=Route met succes tot stand gebracht op {0} +connection.route.failNotification=Failed to create connection on {1}: {2} [[[edit]](({0}))]. +connection.route.disableNotification=Verbinding via route verbroken op {0} +connection.route.pauseNotification=verbinding onderbroken +connection.route.resumeNotification=verbinding hervat op +connection.test.sent=Testbericht met succes verzonden naar{0} via{1} +connection.route.exception={,1} +# Connection exception messages +connection.error.org.smslib.alreadyconnectedexception=Apparaat is al verbonden +connection.error.org.smslib.gsmnetworkregistrationexception=Registratie bij GSM network is mislukt +connection.error.org.smslib.invalidpinexception=Ingevoerde PIN incorrect +connection.error.org.smslib.nopinexception=PIN vereist maar niet ingevoerd +connection.error.org.smslib.notconnectedexception={0} +connection.error.org.smslib.nosuchportexception=Port niet gevonden, of onbereikbaar +connection.error.java.io.ioexception=Port heeft een fout veroorzaakt: {0} +connection.error.frontlinesms2.camel.exception.invalidapiidexception={0} +connection.error.frontlinesms2.camel.exception.authenticationexception={0} +connection.error.frontlinesms2.camel.exception.insufficientcreditexception={0} +connection.error.serial.nosuchportexception=Port niet gevonden +connection.error.org.apache.camel.runtimecamelexception=Kan geen verbinding tot stand brengen +connection.error.onsave={0} +connection.header=Settings > Connections +connection.list.none=U heeft geen verbindingen geconfigureerd. +connection.edit=Verbinding bewerken +connection.delete=Verbinding verwijderen +connection.deleted=Verbinding {0} is verwijderd. +connection.route.enable=Activeren +connection.route.retryconnection=Opnieuw proberen +connection.add=Nieuwe verbinding toevoegen +connection.createtest.message.label=Testbericht +connection.route.disable=Route verwijderen +connection.send.test.message=Test SMS verzenden +connection.test.message=Gefeliciteerd namens FrontlineSMS \\o/ U heeft {0} met succes samengesteld en U kunt nu een SMS verzenden\\o/ +connection.validation.prompt=Alle verplichte velden invullen a.u.b. +connection.select=Verbindingstype kiezen +connection.type=Type kiezen +connection.details=Gegevens invoeren +connection.confirm=Bevestigen +connection.createtest.number=Nummer +connection.confirm.header=Instellingen bevestigen +connection.name.autoconfigured=Automatisch geconfigureerd {0} {1} op port {2}" +status.connection.title=Verbindingen +status.connection.manage=Verbindingen beheren +status.connection.none=U heeft geen verbindingen geconfigureerd +status.devises.header=Apparatuur bespeurd +status.detect.modems=Modems bespeuren +status.modems.none=Nog geen apparatuur bespeurd. +status.header=Usage Statistics +connectionstatus.connecting=Verbinding wordt tot stand gebracht +connectionstatus.connected=Verbonden +connectionstatus.disabled=Route verwijderen +connectionstatus.failed=Mislukt +connectionstatus.not_connected=Niet verbonden +default.doesnt.match.message=Eigenschap [{0}] van klasse [{1}] met waarde [{2}] komt niet overeen met het vereiste pattern [{3}] +default.invalid.url.message=Eigenschap [{0}] van klasse [{1}] met waarde [{2}] is geen geldige URL +default.invalid.creditCard.message=Eigenschap [{0}] van klasse [{1}] met waarde [{2}] is geen geldig credit card nummer +default.invalid.email.message=Eigenschap [{0}] van klasse [{1}] met waarde [{2}] is is geen geldig e-mailadres +default.invalid.range.message=Eigenschap [{0}] van klasse [{1}] v [{2}] valt niet in de geldige waardenreeks van [{3}] tot [{4}] +default.invalid.size.message=Eigenschap [{0}] van klasse [{1}] v [{2}] valt niet in de geldige grootte van [{3}] tot [{4}] +default.invalid.max.message=Eigenschap [{0}] van klasse [{1}] met waarde [{2}] overschrijdt de maximumwaarde [{3}] +default.invalid.min.message=Eigenschap [{0}] van klasse [{1}] met waarde [{2}] is minder dan de minimumwaarde [{3}] +default.invalid.max.size.message=Eigenschap [{0}] van klasse [{1}] v [{2}] overschrijdt de maximumgrootte van [{3}] +default.invalid.min.size.message=Eigenschap [{0}] van klasse [{1}] v [{2}] is minder dan minimumgrootte van [{3}] +default.invalid.validator.message=Eigenschap [{0}] van klasse [{1}] v [{2}] is niet geldig +default.not.inlist.message=Eigenschap [{0}] van klasse [{1}] met waarde [{2}] komt niet voor in de lijst [{3}] +default.blank.message=Eigenschap [{0}] van klasse [{1}] mag niet leeg zijn +default.not.equal.message=Eigenschap [{0}] van klasse [{1}] met waarde [{2}] cannot equal [{3}] +default.null.message=Eigenschap [{0}] van klasse [{1}] mag niet leeg zijn +default.not.unique.message=Eigenschap [{0}] van klasse [{1}] met waarde [{2}] moet uniek zijn +default.paginate.prev=Vorige +default.paginate.next=Volgende +default.boolean.true=Juist +default.boolean.false=Onjuist +default.date.format=dd MMMM, yyyy hh:mm +default.number.format=0 +default.unarchived={0} gede-archiveerd +default.unarchive.failed=De-archiveren mislukt {0} +default.restored={0} teruggezet +default.restore.failed=kon niet worden teruggezet {0} met id {1} +default.archived.multiple={0} gearchiveerd +default.created={0} Aangemaakt +default.created.message={0} {1} is aangemaakt +default.create.failed=Aanmaken is mislukt {0} +default.updated={0} is bijgewerkt +default.update.failed=Bijwerking mislukt {0} met id {1} +default.updated.multiple={0} is bijgewerkt +default.updated.message={0} bijgewerkt +default.deleted={0} gewist +default.trashed={0} moved to trash +default.trashed.multiple={0} verwijderd naar de prullenmand +default.archived={0} archived +default.unarchive.keyword.failed=De-archiveren {0} mislukt. Trefwoord of naam in gebruik +default.unarchived.multiple={0} gede-archiveerd +default.delete.failed=Kon niet worden verwijderd {0} met id {1} +default.notfound=Niet gevonden {0} met id {1} +default.optimistic.locking.failure=Een andere gebruiker heeft dit bijgewerkt {0} tijdens Uw bewerking +default.home.label=Start +default.list.label={0} Lijst +default.add.label=Toevoegen {0} +default.new.label=Nieuw {0} +default.create.label=Aanmaken {0} +default.show.label=Zichtbaar maken {0} +default.edit.label=Bewerken {0} +search.clear=Zoekactie annuleren +default.button.create.label=Aanmaken +default.button.edit.label=Bewerken +default.button.update.label=Bijwerken +default.button.delete.label=Verwijderen +default.button.search.label=Zoeken +default.button.apply.label=Toepassen +default.button.delete.confirm.message=Bent U zeker? +default.deleted.message={0} verwijderd +# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) +typeMismatch.java.net.URL=Eigenschap {0} moet een geldige URL zijn +typeMismatch.java.net.URI=Eigenschap {0} moet een geldige URL zijn +typeMismatch.java.util.Date=Eigenschap {0} oet een geldige datum zijn +typeMismatch.java.lang.Double=Eigenschap {0} moet een geldig nummer zijn +typeMismatch.java.lang.Integer=Eigenschap {0} moet een geldig nummer zijn +typeMismatch.java.lang.Long=Eigenschap {0} moet een geldig nummer zijn +typeMismatch.java.lang.Short=Eigenschap {0} moet een geldig nummer zijn +typeMismatch.java.math.BigDecimal=Eigenschap {0} moet een geldig nummer zijn +typeMismatch.java.math.BigInteger=Eigenschap {0} v +typeMismatch.int={0} moet een geldig nummer zijn +# Application specific messages +messages.trash.confirmation=Dit zal de prullenbak legen en de berichten definitief verwijderen. Wilt u doorgaan? +default.created.poll=De poll is aangemaakt! +default.search.label=Zoekfunctie annuleren +default.search.betweendates.title=Tussen datums: +default.search.moresearchoption.label=Verdere zoekopties +default.search.date.format=d/M/yyyy +default.search.moreoption.label=Verdere opties +# SMSLib Fconnection +smslib.label=Telefoon/Modem +smslib.type.label=Soort +smslib.name.label=Naam +smslib.manufacturer.label=Fabrikant +smslib.model.label=Model +smslib.port.label=Port +smslib.baud.label=Baudrate +smslib.pin.label=PIN +smslib.imsi.label=SIM IMSI +smslib.serial.label=Serienummer van het apparaat # +smslib.sendEnabled.label=Gebruiken voor versturen +smslib.receiveEnabled.label=Gebruiken voor ontvangen +smslibFconnection.sendEnabled.validator.error.send=Modem gebruiken om te versturen +smslibFconnection.receiveEnabled.validator.error.receive=of berichten te ontvangen +smslib.description=Verbinden met USB, serial en bluetooth modems of telefoon +smslib.global.info=FrontlineSMS zal proberen om automatisch een verbonden modem of telefoon te configureren, maar hier kunt U deze zelf configureren +# Email Fconnection +email.label=E-mail +email.type.label=Soort +email.name.label=Naam +email.receiveProtocol.label=Protocol +email.serverName.label=Server Naam +email.serverPort.label=Serverpoort +email.username.label=Gebruikersnaam +email.password.label=Wachtwoord +# CLickatell Fconnection +clickatell.label=Clickatell Account +clickatell.type.label=Type +clickatell.name.label=Naam +clickatell.apiId.label=API ID +clickatell.username.label=Gebruikersnaam +clickatell.password.label=Wachtwoord +clickatell.sendToUsa.label=Verzonden naar de VS +clickatell.fromNumber.label=Van Nummer +clickatell.description=Een bericht verzenden en ontvangen via een Clickatell account +clickatell.global.info=U zult een account moeten configureren met Clickatell (www.clickatell.com). +clickatellFconnection.fromNumber.validator.invalid=A 'Van Nummer' is vereist om een bericht naar de Verenigde Staten te verzenden +# TODO: Change markup below to markdown +clickatell.info-local=Om een Clickatell verbinding tot stand te brengen moet men een Clickatell account hebben. Ga naar om u in te schrijven voor een 'Developer's Central Account' als u nog geen account heeft. U kunt gratis inschrijven voor een testboodschap en het zal niet langer dan 5 minuten in beslag nemen.

Zodra u een Clickatell account heeft, gaat u naar 'Create a Connection (API ID)' op de eerste pagina. Kies 'APIs,' dan 'Set up a new API.' Kies 'add HTTP API' met de default settings en voer in de desbetreffende informatie.

De 'Name' field is uw referentie voor uw Frontline account, en betreft niet het Clickatell API, b.v. 'Mijn plaatselijke boodschap verbinding'. +clickatell.info-clickatell=De volgende informatie moet gekopieerd en geplakt worden direct van het Clikatell HTTP API scherm. +#Nexmo Fconnection +nexmo.label=Nexmo +nexmo.type.label=Mexmo verbinding +nexmo.name.label=Naam +nexmo.api_key.label=API Sleutel: +nexmo.api_secret.label=API geheim +nexmo.fromNumber.label=Van Nummer +nexmo.description=Een bericht verzenden en ontvangen via een Clickatell account +nexmo.receiveEnabled.label=Ontvangen ingeschakeld +nexmo.sendEnabled.label=Verzenden ingeschakeld +# Smssync Fconnection +smssync.label=SMSSync +smssync.name.label=Naam +smssync.type.label=Type +smssync.receiveEnabled.label=Ontvangen ingeschakeld +smssync.sendEnabled.label=Verzenden ingeschakeld +smssync.secret.label=Geheime informatie +smssync.timeout.label=Timeout (min) +smssync.description=Gebruik een Android telefoon met een geinstalleerde Smssync app om een SMS with FrontlineSMS to ontvangen en verzenden +smssync.field.secret.info=Op Uw app, voer de geheime informatie in die overeenkomt met dit veld. +smssync.global.info=Download de SMSSync app vansmssync.ushahidi.com +smssync.timeout=De Android telefoon gekoppeld aan "{0}" heeft geen verbinding gezocht met uw Frontline account gedurende {1} minute(s) [edit] +smssync.info-setup=Frontline producten stellen u in staat boodschappen te versturen en ontvangen via uw Android telefoon. Daartoe dient u o:\n\n1. Een 'Geheim' en een naam voor uw verbinding in te voeren. Een geheim is een wachtoord van uw keuze.\n2. Download en installeer [SMSSync van de Android App store](https://play.google.com/store/apps/details?id=org.addhen.smssync&hl=en) op uw Android telefoon\n3. Zodra u deze verbinding tot stand heeft gebracht kunt u a nieuwe Sync URL maken in SMSSync op uw Android telefoon door de verbinding URL in te voeren (gegenereed door uw Frontline product en te zien op de volgende pagina) en uw gekozen geheim. Zie [The SMSSync Site](http://smssync.ushahidi.com/howto) voor meer hulp. +smssync.info-timeout=If SMSSync does not contact your Frontline product for a certain duration (default 60 minutes), your queued messages will NOT be sent, and you will see a notification that the messages failed to send. Select this duration below: +smssync.info-name=Finally, you should name your SMSSync connection with a name of your choice, e.g. 'Bob's work Android'. +# Messages Tab +message.create.prompt=bericht invoeren +message.character.count=resterende tekens {0} ({1} SMS message(s)) +message.character.count.warning=Kan langer zijn na ingevoerde vervangingen +message.header.inbox=Postvak IN +message.header.sent=Verzonden +message.header.pending=In behandeling +message.header.trash=Prullenbak +message.header.folder=Mappen +message.header.activityList=ActivityList +message.header.folderList=FolderList +announcement.label=Aankondiging +announcement.description=Verstuur een aankondigingsbericht en organiseer de antwoorden +announcement.info1=De aankondiging is opgeslagen en de berichten zijn toegevoegd aan de wachtrij +announcement.info2=Het kan enige tijd duren voordat alle berichten verzonden zijn,afhankelijk van het aantal berichten en de netwerk verbinding. +announcement.info3=Om het verloop van de berichten te zien, open de'In behandeling' berichten map +announcement.info4=Om de aankondiging te zien, klik op het in het menu aan de linkerkant. +announcement.validation.prompt=Alle verplichte velden invullen a.u.b. +announcement.select.recipients=Selecteer de ontvangers +announcement.confirm=Bevestigen +announcement.delete.warn=verwijderen {0} WAARSCHUWING: Dit kan niet verwijderd worden ! +announcement.prompt=Geef deze aankondiging een naam +announcement.confirm.message=bericht +announcement.details.label=Gegevens bevestigen +announcement.message.label=bericht +announcement.message.none=geen +announcement.recipients.label=Ontvangers +announcement.create.message=bericht aanmaken +#TODO embed javascript values +announcement.recipients.count=geselecteerde contactnamen +announcement.messages.count=berichten worden verzonden +announcement.moreactions.delete=Aankondiging verwijderen +announcement.moreactions.rename=Aankondiging een nieuwe naam geven +announcement.moreactions.edit=Aankondiging bewerken +announcement.moreactions.export=Aankondiging exporteren +frontlinesms2.Announcement.name.unique.error.frontlinesms2.Announcement.name={1} {0} "{2}" moet uniek zijn +archive.inbox=Postvak IN archief +archive.sent=Verstuurd archief +archive.activity=Activiteit archief +archive.folder=Folder archief +archive.folder.name=Naam +archive.folder.date=Datum +archive.folder.messages=berichten +archive.folder.none=  Geen gearchiveerde folders +archive.activity.name=Naam +archive.activity.type=Type +archive.activity.date=Datum +archive.activity.messages=berichten +archive.activity.list.none=  Geen gearchiveerde activititeiten +archive.header=Archief +autoreply.enter.keyword=Voer het trefwoord in +autoreply.create.message=Voer het automatische antwoord in +activity.autoreply.sort.description=Als men berichten verzendt die beginnen met een specifiek trefwoord dan kan FrontlineSMS automatisch de berichten in uw systeem verwerken +activity.autoreply.disable.sorting.description=berichten worden niet automatisch gesorteerd en beantwoordtin deze activiteit +autoreply.confirm=Bevestigen +autoreply.name.label=bericht +autoreply.details.label=De gegevens bevestigen +autoreply.label=automatisch antwoord +autoreply.keyword.label=trefwoord(en) +autoreply.description=Automatisch beantwoorden van inkomende berichten +autoreply.info=Het automatische antwoord is gemaakt, berichten die beginnen met Uw trefwoord worden automatisch beantwoordt met deze bericht en op een lijst geplaatst die U kunt bezichtigen door erop te klikken in het menu aan de rechterkant. +autoreply.info.warning=Automatische antwoorden zonder trefwoord gaan naar alle andere inkomende berichten. +autoreply.info.note=Note: Als u het automatische antwoord archiveert dan worden de inkomende berichten niet langer gesorteerd. +autoreply.validation.prompt=Alle verplichte velden invullen a.u.b. +autoreply.message.title=Bericht te verzenden voor dit automatische antwoord: +autoreply.keyword.title=Berichten automatisch sorteren met een trefwoord : +autoreply.name.prompt=Geef dit automatisch antwoord een naam +autoreply.message.count=0 tekens (1 SMS bericht) +autoreply.moreactions.delete=Verwijder het automatische antwoord +autoreply.moreactions.rename=Wijzig de naam van het automatische antwoord +autoreply.moreactions.edit=Bewerk het automatische antwoord +autoreply.moreactions.export=Exporteer het automatische antwoord +autoreply.all.messages=Gebruik het trefwoord niet (Alle inkomende boodscappen ontvangen hetzelfde automatische antwoord) +autoreply.text.none=Geen +frontlinesms2.Autoreply.name.unique.error.frontlinesms2.Autoreply.name={1} {0} "{2}" moet uniek zijn +frontlinesms2.Autoreply.name.validator.error.frontlinesms2.Autoreply.name=Naam van het automatische antwoord moet uniek zijn +frontlinesms2.Keyword.value.validator.error.frontlinesms2.Autoreply.keyword.value=Trefwoord "{2}" is reeds in gebruik +frontlinesms2.Autoreply.autoreplyText.nullable.error.frontlinesms2.Autoreply.autoreplyText=bericht kan niet leeg zijn +autoforward.title={0} +autoforward.label=Automatisch Doorsturen +autoforward.description=Automatisch doorsturen van inkomende berichten naar contacten +autoforward.recipientcount.current=Momenteel {0} ontvangers +autoforward.create.message=Bericht invoeren +autoforward.confirm=Bevestigen +autoforward.recipients=Ontvangers +autoforward.name.prompt=Geef deze Automatisch Doorsturen activiteit een naam +autoforward.details.label=Gegevens bevestigen +autoforward.keyword.label=Trefwoord(en) +autoforward.name.label=Bericht +autoforward.contacts=Contacten +autoforward.groups=Groepen +autoforward.info=The Automatisch Doorsturen procedure is gemaakt, alle berichten met Uw trefwoord worden toegevoegd aan deze Automatisch Doorsturen activiteit die te bezichtigen is door erop te klikken in het menu aan de rechterkant. +autoforward.info.warning=Een automatisch doorsturen procedure zonder trefwoord heeft als gevolg dat alle inkomende berichten worden doorgestuurd +autoforward.info.note=Note: Als U het Automatisch Doorsturen archiveert zullen de inkomende berichten niet langer gesorteerd worden. +autoforward.save=Het Automatisch Doorsturen is opgeslagen! +autoforward.save.success={0} Automatisch Doorsturen is opgeslagen! +autoforward.global.keyword=Geen (alle inkomende berichten worden verwerkt) +autoforward.disabled.keyword=Geen (automatisch doorsturen uitgeschakeld +autoforward.keyword.none.generic=Geen +autoforward.groups.none=Geen +autoforward.contacts.none=Geen +autoforward.message.format=Bericht +contact.new=Nieuw contactpersoon +contact.list.no.contact=Geen contacten hier! +contact.header=Contacten +contact.header.group=Contacten >> {0} +contact.all.contacts=All contacten ({0}) +contact.create=Nieuw contact maken +contact.groups.header=Groepen +contact.create.group=Nieuwe groep maken +contact.smartgroup.header=Smart Groups +contact.create.smartgroup=Nieuwe Smart Group maken +contact.add.to.group=Toevoegen aan de groep... +contact.remove.from.group=Verwijderen uit de groep +contact.customfield.addmoreinformation=Informatie toevoegen... +contact.customfield.option.createnew=Nieuwe maken... +contact.name.label=Naam +contact.phonenumber.label=Telefoonnummer +contact.notes.label=Notities +contact.email.label=E-mail +contact.groups.label=Groepen +contact.notinanygroup.label=Niet gevonden in groepen +contact.messages.label=Berichten +contact.messages.sent={0} berichten zijn verzonden +contact.received.messages={0} ontvangen berichten +contact.search.messages=Zoek berichten +contact.select.all=Selecteer alles +contact.search.placeholder=Search your contacts, or enter phone numbers +contact.search.contact=Contacten +contact.search.smartgroup=Smart Groups +contact.search.group=Groepen +contact.search.address=Telefoonnummer toevoegen: +contact.not.found=Contact niet gevonden +group.not.found=Groep niet gevonden +smartgroup.not.found=Smart Group niet gevonden +group.rename=Groep nieuwe naam geven +group.edit=Groep bewerken +group.delete=Groep verwijderen +group.moreactions=Andere handelingen... +customfield.validation.prompt=Een naam invoeren a.u.b. +customfield.validation.error=Deze naam is al in gebruik +customfield.name.label=Naam +export.contact.info=Om contacten te exporteren van FrontlineSMS, kies welk soort export en welke informatie die moet worden opgenomen in de geexporteerde data. +export.message.info=Om berichten te exporteren van FrontlineSMS, kies welk soort export en de informatie die moet worden opgenomen in de geexporteerde data. +export.selectformat=Selecteer een uitvoerindeling +export.csv=CSV indeling te gebruiken in het spreadsheet +export.pdf=PDF indeling om af te drukken +folder.name.label=Naam +group.delete.prompt=Bent U zeker dat U {0} wilt verwijderen? WAARSCHUWING: Dit kan niet ongedaan gemaakt worden +layout.settings.header=Instellingen +activities.header=Activiteiten +activities.create=Een nieuwe activiteit aanmaken +folder.header=Mappen +folder.create=Een nieuw map maken +folder.label=Map +message.folder.header={0} Map +fmessage.trash.actions=Prullenbak acties... +fmessage.trash.empty=Prullenbak leegmaken +fmessage.to.label=Aan +trash.empty.prompt=Alle berichten en activiteiten in the prullenbak zullen permanent verwijderd worden +fmessage.responses.total={0} antwoorden in totaal +fmessage.label=Bericht +fmessage.label.multiple={0} berichten +poll.prompt=Geef deze poll een naam +poll.details.label=Bevestig de gegevens +poll.message.label=Bericht +poll.choice.validation.error.deleting.response=Een opgeslagen keus mag geen lege waarde hebben +poll.alias=Aliassen +poll.keywords=Trefwoorden +poll.aliases.prompt=Voer de aliassen in voor de desbetreffende opties. +poll.keywords.prompt.details=Het top-level trefwoord zal de poll benamen en verzonden worden in de poll instructiebericht. Ieder antwoord kan ook andere short-cut trefwoorden bevatten. +poll.keywords.prompt.more.details=U kunt meerdere door commas gescheiden trefwoorden invoeren voor de top-level en antwoorden. Als eronder geen toplevel trefwoorden zijn ingevoerd, dan moeten de trefwoorden in het antwoord uniek zijn in alle activiteiten. +poll.keywords.response.label=Antwoord trefwoorden +poll.response.keyword=Antwoord trefwoorden instellen +poll.set.keyword=Een top-level trefwoord instellen +poll.keywords.validation.error=Trefwoorden moeten uniek zijn +poll.sort.label=Autosorteren +poll.autosort.no.description=De antwoorden niet automatisch sorteren. +poll.autosort.description=De antwoorden automatisch sorteren. +poll.sort.keyword=trefwoord +poll.sort.toplevel.keyword.label=Top-level trefwoord(en) (optioneel) +poll.sort.by=Sorteren naar +poll.autoreply.label=automatisch antwoord +poll.autoreply.none=geen +poll.recipients.label=Ontvangers +poll.recipients.none=Geen +poll.toplevelkeyword=Top-level trefwoorden +poll.sort.example.toplevel=bv. TEAM +poll.sort.example.keywords.A=bv. A, AMAZING +poll.sort.example.keywords.B=bv. B, BEAUTIFUL +poll.sort.example.keywords.C=bv. C, COURAGEOUS +poll.sort.example.keywords.D=bv. D, DELIGHTFUL +poll.sort.example.keywords.E=bv. E, EXEMPLARY +poll.sort.example.keywords.yn.A=bv. YES, YAP +poll.sort.example.keywords.yn.B=bv. No, NOP +#TODO embed javascript values +poll.recipients.count=geselecteerde contacten +poll.messages.count=berichten zullen worden verzonden +poll.yes=Ja +poll.no=Nee +poll.label=Poll +poll.description=Stuur een vraag en analyseer de antwoorden +poll.messages.sent={0} berichten zijn verzonden +poll.response.enabled=Automatisch antwoord ingeschakeld +poll.message.edit=Bewerk de bericht die verzonden wordt naar de ontvangers +poll.message.prompt=De volgende bericht wordt verzonden naar de ontvangers van de poll +poll.message.count=Resterende tekens 160 (1 SMS message) +poll.moreactions.delete=Poll verwijderen +poll.moreactions.rename=Poll een nieuwe naam geven +poll.moreactions.edit=Poll bewerken +poll.moreactions.export=Poll exporteren +folder.moreactions.delete=Map verwijderen +folder.moreactions.rename=Map een nieuwe naam geven +folder.moreactions.export=Map exporteren +#TODO embed javascript values +poll.reply.text=Reply "{0}" voor Ja, "{1}" voor Nee. +poll.reply.text1={0} "{1}" voor {2} +poll.reply.text2='Ja' of 'Nee'antwoorden a.u.b. +poll.reply.text3=of +poll.reply.text5=Antwoord +poll.reply.text6=Beantwoorden a.u.b. +poll.message.send={0} {1} +poll.recipients.validation.error=Selecteer contacten om berichten naar te verzenden +frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name={1} {0} "{2}" moet uniek zijn +frontlinesms2.Poll.responses.validator.error.frontlinesms2.Poll.responses=De opties voor beantwoording mogen niet identiek zijn +frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value=Trefwoord"{2}" is reeds in gebruik +wizard.title.new=Nieuw +wizard.fmessage.edit.title=Bewerken {0} +popup.title.saved={0} opgeslagen! +popup.activity.create=Create Nieuwe Activiteit : Selecteer type +popup.smartgroup.create=Maak een Smart Group +popup.help.title=Help +smallpopup.customfield.create.title=Maak een Aangepast Veld +smallpopup.group.rename.title=Geef de groep een nieuwe naam +smallpopup.group.edit.title=De groep bewerken +smallpopup.group.delete.title=De groep verwijderen +smallpopup.fmessage.rename.title=Een nieuwe naam geven {0} +smallpopup.fmessage.delete.title=Verwijderen {0} +smallpopup.fmessage.export.title=Exporteren +smallpopup.delete.prompt=Verwijderen {0} ? +smallpopup.delete.many.prompt=Contacten {0} verwijderen? +smallpopup.empty.trash.prompt=Prullenbak leegmaken? +smallpopup.messages.export.title=Exporteer Results ({0} berichten +smallpopup.test.message.title=Test bericht +smallpopup.recipients.title=Ontvangers +smallpopup.folder.title=Map +smallpopup.contact.export.title=Exporteren +smallpopup.contact.delete.title=Verwijderen +contact.selected.many={0} contacten geselecteerd +group.join.reply.message=Welkom +group.leave.reply.message=Tot ziens +fmessage.new.info=U heeft {0} nieuwe berichten. Klik om weer te geven +wizard.quickmessage.title=Send Message +wizard.messages.replyall.title=Allemaal beantwoorden +wizard.send.message.title=Bericht verzenden +wizard.ok=Ok +wizard.create=Maken +wizard.save=Opslaan +wizard.send=Verzenden +common.settings=Instellingen +common.help=Help +validation.nospaces.error=Trefwoord mag geen spaties bevatten +activity.validation.prompt=Alle verplichte velden invullen a.u.b. +validator.invalid.name=Een andere activiteit heeft deze naam al {2} +autoreply.blank.keyword=Leeg trefwoord. Alle inkomende berichten krijgen een antwoord. +poll.type.prompt=Selecteer welk soort poll aan te maken +poll.question.yes.no=Vraag met een 'Ja' of 'Nee' antwoord +poll.question.multiple=Meerkeuzevraag (bv. 'Rood', 'Blauw', 'Groen') +poll.question.prompt=Vraag invoeren +poll.message.none=Geen vraag verzenden voor deze poll (alleen antwoorden verzamelen). +poll.replies.header=automatisch antwoord van poll antwoorden (optioneel) +poll.replies.description=Als een inkomend bericht herkend wordt als een poll antwoord, een bericht sturen naar de persoon die het antwoord ingezonden heeft. +poll.autoreply.send=Poll antwoorden met een automatisch antwoord beantwoorden +poll.responses.prompt=Mogelijke antwoorden invoeren (tussen 2 en 5) +poll.sort.header=Antwoorden utomatisch sorteren met behulp van een trefwoord (optioneel) +poll.sort.enter.keywords=Trefwoord invoeren vorr de poll en de antwoorden +poll.sort.description=Als gebruikders antwoorden sturen met een specifiek trefwoord dan kan FrontlineSMS de berichten automatisch sorteren in uw systeem. +poll.no.automatic.sort=De berichten niet automatissch sorteren +poll.sort.automatically=De berichten met het volgende trefwoord automatisch sorteren +poll.validation.prompt=Alle verplichte velden invullen a.u.b. +poll.name.validator.error.name=Poll benamingen moeten uniek zijn +pollResponse.value.blank.value=Poll antwoord waarde mag niet leeg zijn +poll.keywords.validation.error.invalid.keyword=Ongeldig trefwoord. Probeer a, naam, woord +poll.question=Vraag invoeren +poll.response=Lijst met antwoorden +poll.sort=Automatisch sorteren +poll.reply=automatisch antwoord +poll.edit.message=Bericht bewerken +poll.recipients=Selecteer ontvangers +poll.confirm=Bevestigen +poll.save=De poll is opgeslagen! +poll.save.success={0} Poll is opgeslagen! +poll.messages.queue=Als u verkozen heeft een bericht met deze poll te verzenden dan zijn de berichten toegevoegd aan de berichten wachtrij. +poll.messages.queue.status=Het kan enige tijd duren voordat alle berichten verzonden zijn, afhankelijk van het aantal berichten en de netwerk verbinding. +poll.pending.messages=Om de status van uw bericht te zien, open de map met de 'In behandeling' berichten +poll.send.messages.none=Geen berichten worden verzonden +quickmessage.details.label=Gegevens bevestigen +quickmessage.message.label=Bericht +quickmessage.message.none=Geen +quickmessage.recipient.label=Ontvanger +quickmessage.recipients.label=Ontvangers +quickmessage.message.count=Resterende ekens 160 (1 SMS bericht +quickmessage.enter.message=Bericht invoeren +quickmessage.select.recipients=Selecteer ontvangers +quickmessage.confirm=Bevestigen +#TODO embed javascript values +quickmessage.recipients.count=geselecteerde contacten +quickmessage.messages.count=berichten worden verzonden +quickmessage.count.label=Aantal berichten: +quickmessage.messages.label=Bericht invoeren +quickmessage.phonenumber.label=Telefoonnummer toevoegen: +quickmessage.phonenumber.add=Toevoegen +quickmessage.selected.recipients=geselecteerde ontvangers +quickmessage.validation.prompt=Alle verplichte velden invullen a.u.b. +fmessage.number.error=Non-numeric characters in this field will be removed when saved +search.filter.label=Zoeken beperken tot +search.filter.group=Selecteer groep +search.filter.activities=Selecteer activiteit/map +search.filter.messages.all=Allen verzonden en ontvangen +search.filter.inbox=Alleen ontvangen berichten +search.filter.sent=Alleen verzonden berichten +search.filter.archive=Archief inbegrepen +search.betweendates.label=Tussen datums +search.header=Zoeken +search.quickmessage=Send message +search.export=Exporteer de resultaten +search.keyword.label=Trefwoord +search.contact.name.label=Contact naam +search.contact.name=Contact naam +search.result.header=Resultaten +search.moreoptions.label=Meer opties +settings.general=Algemeen +settings.porting=Import and Export +settings.connections=Telefoons & verbindingen +settings.logs=Systeem +settings.general.header=Instellingen > Algemeen +settings.logs.header=Settings > System Logs +logs.none=U heeft geen logboeken. +logs.content=Bericht +logs.date=Tijd +logs.filter.label=Toon logboeken voor +logs.filter.anytime=alle tijden +logs.filter.days.1=afgelopen 24 uur +logs.filter.days.3=laatste 3 dagen +logs.filter.days.7=laatste 7 dagen +logs.filter.days.14=laatste 14 dagen +logs.filter.days.28=laatste 28 dagen +logs.download.label=Download systeem logboeken +logs.download.buttontext=Download Logboeken +logs.download.title=Download logboeken om te verzenden +logs.download.continue=Doorgaan +smartgroup.validation.prompt=Alle verplichte velden invullen a.u.b. Een enkele regel per veld opgeven. +smartgroup.info=Om een Smart group te maken moet u de filtercriteria opgeven die van toepassing zijn voor deze groep +smartgroup.contains.label=houdt in +smartgroup.startswith.label=begint met +smartgroup.add.anotherrule=Voeg nog een regel toe +smartgroup.name.label=Naam +modem.port=Port +modem.description=Beschrijving +modem.locked=Vergrendeld? +traffic.header=Verkeer +traffic.update.chart=Grafiek bijwerken +traffic.filter.2weeks=Toont de laatste twee weken +traffic.filter.between.dates=Tussen datums +traffic.filter.reset=Filters opnieuw instellen +traffic.allgroups=Alle groepen tonen +traffic.all.folders.activities=Alle activiteiten/mappen tonen +traffic.sent=Verzonden +traffic.received=Ontvangen +traffic.total=Totaal +tab.message=Berichten +tab.archive=Archief +tab.contact=Contacten +tab.status=Usage Statistics +tab.search=Zoeken +help.info=Deze versie is een beta dus er is geen ingebouwde Help. Voor hulp in dit stadium raadpleeg de gebruikersforums. +help.notfound=Dit 'help' bestand is helaas nog niet beschikbaar. +# IntelliSms Fconnection +intellisms.label=IntelliSms Account +intellisms.type.label=Type +intellisms.name.label=Naam +intellisms.username.label=Gebruikersnaam +intellisms.password.label=Wachtwoord +intellisms.sendEnabled.label=Gebruiken om te verzenden +intellisms.receiveEnabled.label=Voor ontvangst +intellisms.receiveProtocol.label=Protocol +intellisms.serverName.label=Server Naam +intellisms.serverPort.label=Serverpoort +intellisms.emailUserName.label=Gebruikersnaam +intellisms.emailPassword.label=Wachtwoord +intellisms.description=Berichten verzenden en ontvangen via een Intellisms account +intellisms.global.info=U moet een account configureren met Intellisms (www.intellisms.co.uk). +intelliSmsFconnection.send.validator.invalid=U kunt geen verbinding configureren zonder VERZENDEN of ONTVANGEN functionaliteit +intelliSmsFconnection.receive.validator.invalid=U kunt geen verbinding configureren zonder VERZENDEN of ONTVANGEN functionaliteit +#Controllers +contact.label=Contact(en) +contact.edited.by.another.user=Een andere gebruiker heeft dit Contact bijgewerkt terwijl u bezig was met bijwerken +contact.exists.prompt=Er bestaat al een contact met dit nummer +contact.exists.warn=Een contact met dit nummer bestaat al +contact.view.duplicate=Duplicaat tonen +contact.addtogroup.error=Toevoegen en verwijderen van dezelfde groep is niet mogelijk! +contact.mobile.label=Mobiel +fconnection.label=Fverbinding +fconnection.name=Fverbinding +fconnection.unknown.type=Onbekend type verbinding: +fconnection.test.message.sent=Testbericht in de wachtrij voor verzending! +announcement.saved=De aankondiging is opgeslagen and de boodschap(pen) zijn in de wachtrij voor verzending geplaatst +announcement.not.saved=De aankondiging kon niet opgeslagen worden! +announcement.save.success={0} Aankondiging is opgeslagen! +announcement.id.exist.not=Kon de aankondiging niet vinden met id {0} +autoreply.save.success={0} automatisch antwoord is opgeslagen! +autoreply.not.saved=automatisch antwoord kon niet worden opgeslagen! +report.creation.error=Rapport is mislukt +export.message.title=FrontlineSMS Bericht Exporteren +export.database.id=DatabaseID +export.message.date.created=Datum wanneer gemaakt +export.message.text=Tekst +export.message.destination.name=Bestemming Naam +export.message.destination.mobile=Bestemming Mobiel +export.message.source.name=Bron Naam +export.message.source.mobile=Bron Mobiel +export.contact.title=FrontlineSMS Contact Exporteren +export.contact.name=Naam +export.contact.mobile=Mobiel +export.contact.email=E-mail +export.contact.notes=Notities +export.contact.groups=Groepen +export.messages.name1={0} {1} ({2} berichten) +export.messages.name2={0} ({1} berichten) +export.contacts.name1={0} groep ({1} contacten) +export.contacts.name2={0} Smart Group ({1} contacten) +export.contacts.name3=Alle contacten ({0} contacten) +folder.archived.successfully=Map is succesvol gearchiveerd! +folder.unarchived.successfully=Map is gede-archiveerd met succes! +folder.trashed=Map is in de prullenbak! +folder.restored=Map is teruggezet! +folder.exist.not=Kon de map niet vinden met id {0} +folder.renamed=Map heeft een nieuwe naam gekregen +group.label=Groep +group.name.label=Naam +group.update.success=Groep is succesvol bijgewerkt +group.save.fail=Groep opslaan is mislukt +group.delete.fail=Groep kon niet verwijderd worden. In gebruik door een abonnement +import.label=Import contacts and messages +import.backup.label=You can use the form below to import your contacts. You can also import messages that you have from a previous Frontline project. +import.prompt.type=Select the type of data you wish to import before uploading +import.messages=Messages in Frontline's CSV format +import.prompt=Select the file containing your data to initiate the import +import.upload.failed=Uploaden bestand niet gelukt door onbekende reden +import.contact.save.error=Er is een fout opgetreden in het opslaan van het contact +import.contact.complete={0} contacten zijn geimporteerd; {1} mislukt +import.contact.exist=The imported contacts already exist. +import.contact.failed.label=mislukte imports contactpersonen +import.contact.failed.info={0} contact(s) successfully imported.
{1} contact(s) could not be imported.
{2} +import.download.failed.contacts=een bestand met de mislukte contactpersonen downloaden +import.message.save.error=Er is een fout opgetreden in het opslaan van het bericht +import.message.complete={0} berichten zijn geimporteerd; {1} mislukt +export.label=Export data from your Frontline workspace +export.backup.label=You can export your Frontline data as VCF/VCard, CSV or PDF +export.prompt.type=Select which data you wish to export +export.allcontacts=All of your contacts +export.inboxmessages=Your Inbox messages +export.submit.label=Export and download data +many.selected={0} {1}s geselecteerd +flash.message.activity.found.not=Activiteit niet gevonden +flash.message.folder.found.not=Map niet gevonden +flash.message=Bericht +flash.message.fmessage={0} bericht(en) +flash.message.fmessages.many={0} SMS berichten +flash.message.fmessages.many.one=1 SMS bericht +fmessage.exist.not=Kon geen bericht vinden met id {0} +flash.message.poll.queued=Poll is opgeslagen and bericht(en) zijn in de verzend wachtrij geplaatst +flash.message.poll.not.saved=Poll kon niet worden opgeslagen! +system.notification.ok=OK +system.notification.fail=MISLUKT +flash.smartgroup.delete.unable=Smart Group kon niet verwijderd worden +flash.smartgroup.saved=Smart group {0} opgeslagen +flash.smartgroup.save.failed=Smart Group opslaan mislukt. Fouten zijn {0} +smartgroup.id.exist.not=Smart Group met id niet gevonden {0} +smartgroup.save.failed=Opslaan mislukt van Smart Group{0}met params {1}{2}fouten: {3} +searchdescriptor.searching=Bezig met zoeken +searchdescriptor.all.messages=alle berichten +searchdescriptor.archived.messages=, inclusief gearchiveerde berichten +searchdescriptor.exclude.archived.messages=, zonder gearchiveerde berichten +searchdescriptor.only=, uitsluitend {0} +searchdescriptor.between=, tussen {0} en{1} +searchdescriptor.from=, van{0} +searchdescriptor.until=, tot{0} +poll.title={0} +announcement.title={0} +autoreply.title={0} +folder.title={0} +frontlinesms.welcome=Welkom bij FrontlineSMS! \\o/ +failed.pending.fmessages={0} bericht(en) in behandeling mislukt. Ga naar de berichten in behandeling voor weergave. +subscription.title={0} +subscription.info.group=Groep: {0} +subscription.info.groupMemberCount={0} leden +subscription.info.keyword=Top-level trefwoorden: {0} +subscription.sorting.disable=Automatisch sorteren uitschakelen +subscription.info.joinKeywords=Deelnemen: {0} +subscription.info.leaveKeywords=Verlaten: {0} +subscription.group.goto=Groep weergeven +subscription.group.required.error=Abonnementen moeten een groep hebben +subscription.save.success={0} Abonnement is opgeslagen! +language.label=Taal +language.prompt=Verander de taal van de FrontlineSMS gebruikersinterface +frontlinesms.user.support=FrontlineSMS Gebruiker Ondersteuning +download.logs.info1=WARNING: Het FrontlineSMS team kunnen de ingediende logs helaas niet rechtstreeks beantwoorden. Als u een gebruiker ondersteuningsverzoek heeft wordt u verzocht de Help files te raadplegen om te zien of u het antwoord daar kunt vinden. Zoniet, reporteer dan uw probleem via onze gebruikers ondersteuningsforums: +download.logs.info2=Andere gebruikers hebben wellicht hetzelfde probleem gerapporteerd en een oplossing gevonden! Om verder te gaan en uw logs in te dienen, klik 'Doorgaan' +# Configuration location info +configuration.location.title=Configuratie Locatie +configuration.location.description=Deze bestanden bevatten uw database en andere instellingen waarvan u waarschijnlijk ergens anders een back-up wil maken +configuration.location.instructions=U kunt de configuratie van uw applicatie in {1}. Deze bestanden bevatten uw database en andere instellingen die u wellicht in een back-upbestand elders wilt opslaan. +dynamicfield.contact_name.label=Contact Naam +dynamicfield.contact_number.label=Contact Nummer +dynamicfield.keyword.label=Trefwoord +dynamicfield.message_content.label=Bericht Inhoud +# TextMessage domain +fmessage.queued=Bericht is in de wachtrij geplaatst om verzonden te worden naar {0} +fmessage.queued.multiple=Bericht is in de wachtrij geplaatst om verzonden te worden naar {0} ontvangers +fmessage.retry.success=Bericht is opnieuw in de wachtrij geplaatst om verzonden te worden naar {0} +fmessage.retry.success.multiple={0} berichten(en) opnieuw in de wachtrij geplaatst om verzonden te worden +fmessage.displayName.label=Naam +fmessage.text.label=Bericht +fmessage.date.label=Datum +fmessage.to=To: {0} +fmessage.to.multiple=To: {0} ontvangers +fmessage.quickmessage=Send message +fmessage.archive=Archiveren +fmessage.activity.archive=Archiveren {0} +fmessage.unarchive=De-archiveren +fmessage.export=Exporteren +fmessage.rename=Nieuwe naam geven {0} +fmessage.edit=Bewerken {0} +fmessage.delete=Delete +fmessage.moreactions=Meer acties... +fmessage.footer.show=Weergeven +fmessage.footer.show.failed=Mislukt +fmessage.footer.show.all=Alles +fmessage.footer.show.starred=Met ster +fmessage.footer.show.incoming=Inkomend +fmessage.footer.show.outgoing=Uitgaand +fmessage.archive.back=Terug +fmessage.activity.sentmessage=({0} berichten verzonden) +fmessage.failed=mislukt +fmessage.header=berichten +fmessage.section.inbox=Postvak IN +fmessage.section.sent=Verzonden +fmessage.section.pending=In behandeling +fmessage.section.trash=Prullenbak +fmessage.addsender=Aan contacten toevoegen +fmessage.resend=Opnieuw verzenden +fmessage.retry=Opnieuw proberen +fmessage.reply=Beantwoorden +fmessage.forward=Doorsturen +fmessage.messages.none=Er zijn geen berichten hier! +fmessage.selected.none=Er is geen bericht geselecteerd +fmessage.move.to.header=Bericht verplaatsten naar... +fmessage.move.to.inbox=Postvak IN +fmessage.archive.many=Archive selected +fmessage.count=1 bericht +fmessage.count.many={0} berichten +fmessage.many=berichten +fmessage.delete.many=Delete selected +fmessage.reply.many=Reply selected +fmessage.restore=Terugzetten +fmessage.restore.many=Terugzetten +fmessage.retry.many=Retry selected +fmessage.selected.many={0} berichten geselecteerd +fmessage.unarchive.many=Unarchive selected +# TODO move to poll.* +fmessage.showpolldetails=Grafiek weergeven +fmessage.hidepolldetails=Grafiek verbergen +# TODO move to search.* +fmessage.search.none=Geen bericht gevonden +fmessage.search.description=Nieuwe zoekactie links beginnen +fmessage.connection.receivedon=Ontvangen op: +activity.name=Naam +activity.delete.prompt=Naar de {0} prullenbak sturen. Alle hieraan gekoppelde berichten zullen nu in de prullenbak geplaatst worden. +activity.label=Activiteit +activity.categorize=Antwoord categoriseren +magicwand.title=Vervangingsexpressies toevoegen +folder.create.success=Map maken succesvol +folder.create.failed=Kon geen map maken +folder.name.validator.error=Naam map reeds in gebruik +folder.name.blank.error=Naam map mag niet leeg zijn +poll.name.blank.error=Poll naam mag niet leeg zijn +poll.name.validator.error=Poll naam reeds in gebruik +autoreply.name.blank.error=Naam automatisch antwoord mag niet leeg zijn +autoreply.name.validator.error=Naam automatisch antwoord reeds in gebruik +announcement.name.blank.error=Naam aankondiging mag niet leeg zijn +announcement.name.validator.error=Naam aankondiging reeds in gebruik +group.name.blank.error=Groepnaam mag niet leeg zijn +group.name.validator.error=Groepnaam reeds in gebruik +#Jquery Validation messages +jquery.validation.required=Dit is een verplicht veld. +jquery.validation.remote=Dit veld corrigeren a.u.b. +jquery.validation.email=Een geldig email adres invullen a.u.b. +jquery.validation.url=Een geldig URL invullen a.u.b. +jquery.validation.date=Een geldige datum invullen a.u.b.. +jquery.validation.dateISO=Een geldige datum invullen a.u.b. (ISO). +jquery.validation.number=Een geldig nummer invullen a.u.b.. +jquery.validation.digits=Uitsluitend nummers invullen a.u.b.. +jquery.validation.creditcard=Een geldig credit card nummer invullen a.u.b.. +jquery.validation.equalto=Dezelfde waarde nogmaals invullen a.u.b.. +jquery.validation.accept=Een waarde met een geldige extensie invullen a.u.b. +jquery.validation.maxlength=Niet meer dan {0} tekens invullen a.u.b. +jquery.validation.minlength=Tenminste {0} tekens invullen a.u.b. +jquery.validation.rangelength=Een waarde tussen {0} en{1} tekenslang invullen a.u.b. +jquery.validation.range=Een waarde tussen {0} en{1} invullen a.u.b. +jquery.validation.max=Een waarde minder dan of gelijk aan {0} invullen a.u.b. +jquery.validation.min=Een waarde meer dan of gelijk aan {0} invullen a.u.b. +# Webconnection common +webconnection.select.type=Selecteer Web Service of toepassing om mee te verbinden: +webconnection.type=Selecteer Type +webconnection.title={0} +webconnection.label=Webverbinding +webconnection.description=Verbinden met web service. +webconnection.sorting=Automatisch sorteren +webconnection.configure=Service configureren +webconnection.api=API weergeven +webconnection.api.info=FrontlineSMS kan geconfigureerd worden om inkomende aanvragen van uw externe service te ontvangen en uitgaande berichten te activeren. Voor meer informatie raadpleeg de online-Help. +webconnection.api.enable.label=API activeren +webconnection.api.secret.label=API geheime sleutel +webconnection.api.disabled=API Uitschakelen +webconnection.api.url=API URL +webconnection.moreactions.retryFailed=retry failed uploads +webconnection.failed.retried=Failed web connections have been scheduled for resending. +webconnection.url.error.locahost.invalid.use.ip=Please use 127.0.0.1 instead of "locahost" for localhost urls +webconnection.url.error.url.start.with.http=Invalid URL (should start with http:// or https://) +# Webconnection - generic +webconnection.generic.label=Andere webservice +webconnection.generic.description=Berichten naar een andere webservice verzenden +webconnection.generic.subtitle=HTTP Webverbinding +# Webconnection - Ushahidi/Crowdmap +webconnection.ushahidi.label=Crowdmap / Ushahidi +webconnection.ushahidi.description=Berichten sturen naar CrowdMap of een Ushahidi server. +webconnection.ushahidi.key.description=De API sleutel voor Crowdmap of Ushahidi kunt u vinden in de Instellingen in de Crowdmap of Ushahidi website. +webconnection.ushahidi.url.label=Adres: +webconnection.ushahidi.key.label=Ushahidi API Sleutel: +webconnection.crowdmap.url.label=Crowdmap installatie adres: +webconnection.crowdmap.key.label=Crowdmap API Sleutel: +webconnection.ushahidi.serviceType.label=Selecteer Service +webconnection.ushahidi.serviceType.crowdmap=Crowdmap +webconnection.ushahidi.serviceType.ushahidi=Ushahidi +webconnection.crowdmap.url.suffix.label=.crowdmap.com +webconnection.ushahidi.subtitle=Webverbinding met {0} +webconnection.ushahidi.service.label=Dienst: +webconnection.ushahidi.fsmskey.label=FrontlineSMS API Geheime informatie: +webconnection.ushahidi.crowdmapkey.label=Crowdmap/Ushahidi API Sleutel: +webconnection.ushahidi.keyword.label=Trefwoord: +url.invalid.url=De opgegeven URL is niet geldig +webconnection.confirm=Bevestigen +webconnection.keyword.title=Alle berichten overbrengen die ontvangen worden en het volgende trefwoord bevatten: +webconnection.all.messages=Trefwoord niet gebruiken (Alle inkomende berichten worden overgebracht naar deze webverbinding +webconnection.httpMethod.label=HTTP Method selecteren: +webconnection.httpMethod.get=GET +webconnection.httpMethod.post=POST +webconnection.name.prompt=Naam van deze webverbinding +webconnection.details.label=Gegevens bevestigen +webconnection.parameters=De informatie die naar de server gestuurd wordt configureren +webconnection.parameters.confirm=Geconfigureerde informatie die naar de server gestuurd is +webconnection.keyword.label=Trefwoord : +webconnection.none.label=Geen +webconnection.url.label=Url Server: +webconnection.param.name=Naam: +webconnection.param.value=Waarde: +webconnection.add.anotherparam=Parameter toevoegen +dynamicfield.message_body.label=Tekst Bericht +dynamicfield.message_body_with_keyword.label=Tekst Bericht met trefwoord +dynamicfield.message_src_number.label=Nummer Contact +dynamicfield.message_src_name.label=Naam Contact +dynamicfield.message_timestamp.label=Tijdstempel Bericht +webconnection.keyword.validation.error=Trefwoord is verplicht +webconnection.url.validation.error=Url is verplicht +webconnection.save=De Webverbinding is opgeslagen! +webconnection.saved=Webverbinding opgeslagen! +webconnection.save.success={0} Webverbinding is opgeslagen! +webconnection.generic.service.label=Dienst: +webconnection.generic.httpMethod.label=Http Methode: +webconnection.generic.url.label=Adres: +webconnection.generic.parameters.label=Geconfigureerde informatie is naar de server gestuurd: +webconnection.generic.keyword.label=Keyword: +webconnection.generic.key.label=API Sleutel: +frontlinesms2.Keyword.value.validator.error.frontlinesms2.UshahidiWebconnection.keyword.value=Ongeldige trefwoord Waarde +#Subscription i18n +subscription.label=Abonnement +subscription.name.prompt=Geef dit abonnement een naam +subscription.details.label=Gegevens bevestigen +subscription.description=Toestaan dat mensen automatisch aan de contact groep deelnemen en verlaten d.m.v. een bericht trefwoord +subscription.select.group=De group selecteren voor het abonnement +subscription.group.none.selected=Geselecteerde groep +subscription.autoreplies=Automatische antwoorden +subscription.sorting=Automatisch sorteren +subscription.sorting.header=Berichten automatisch verwerken met behulp van een trefwoord (optioneel) +subscription.confirm=Bevestigen +subscription.group.header=Groep selecteren +subscription.group.description=Contacten kunnen automatisch aan de groepen toegevoegd of eruit verwijderd worden als FrontlineSMS een bericht ontvangt dat een speciaal trefwoord bevat. +subscription.keyword.header=Trefwoorden invoeren voor dit abonnement +subscription.top.keyword.description=De top-level trefwoorden invoeren die de gebruikers zullen gebruiken om deze groep te selecteren. +subscription.top.keyword.more.description=U kunt meerdere top-level trefwoorden invoeren per optie, door komma's gescheiden. Top-level trefwoorden moeten uniek zijn voor iedere activiteit. +subscription.keywords.header=Voer trefwoorden in voor het deelnemen of verlaten van deze groep. +subscription.keywords.description=U kunt meerdere top-level trefwoorden invoeren, door komma's gescheiden. Als hierboven geen top-level trefwoorden zijn ingevoerd dan moeten deze trefwoorden voor het deelnemen en verlaten uniek zijn voor iedere activiteit. +subscription.default.action.header=Een actie selecteren in het geval dat er geen trefwoord gestuurd wordt +subscription.default.action.description=De gewenste actie selecteren als een bericht het top-level trefwoord bevat maar geen trefwoord vorr het deelnemen of verlaten : +subscription.keywords.leave=Trefwoord(en) voor het verlaten +subscription.keywords.join=Trefwoord(en) om deel te nemen +subscription.default.action.join=Het contact toevoegen aan de groep +subscription.default.action.leave=Het contact verwijderen uit de groep +subscription.default.action.toggle=Het lidmaatschap van het contact in/uitschakelen +subscription.autoreply.join=Een automatisch antwoord sturen als een contact aan de groep deelneemt +subscription.autoreply.leave=Een automatisch antwoord sturen als een contact de groep verlaat +subscription.confirm.group=Groep +subscription.confirm.keyword=Trefwoord +subscription.confirm.join.alias=Trefwoorden deelname +subscription.confirm.leave.alias=Trefwoorden verlaten +subscription.confirm.default.action=Standaardactie +subscription.confirm.join.autoreply=Automatisch antwoord deelname +subscription.confirm.leave.autoreply=Automatisch antwoord verlaten +subscription.info1=Het abonnement is opgeslagen en nu actief +subscription.info2=Inkomende berichten die overeenkomen met dit trefwoord zullen nu het lidmaatschap van het conctact veranderen zoals gedefinieerd +subscription.info3=Klik op het abonnement in het menu links om het te zien +subscription.categorise.title=Berichten categoriseren +subscription.categorise.info=Selecteer de gewenste actie met de afzenders van het(de) geselecteerde bericht(en) wanneer ze toegevoegd worden aan {0} +subscription.categorise.join.label=Afzenders toevoegen aan {0} +subscription.categorise.leave.label=Afzenders verwijderen van {0} +subscription.categorise.toggle.label=In/uitschakelen afzenders' lidmaatschap van {0} +subscription.join=Deelnemen +subscription.leave=Verlaten +subscription.sorting.example.toplevel=e.g OPLOSSING +subscription.sorting.example.join=e.g LID WORDEN, DEELNEMEN +subscription.sorting.example.leave=e.g OPZEGGEN, VERLATEN +subscription.keyword.required=Trefwoord is verplicht +subscription.jointext.required=Tekst automatisch antwoord deelname invoeren a.u.b. +subscription.leavetext.required=Tekst automatisch antwoord verlaten invoeren a.u.b. +subscription.moreactions.delete=Abonnement verwijderen +subscription.moreactions.rename=Abonnement een nieuwe naam geven +subscription.moreactions.edit=Abonnement bewerken +subscription.moreactions.export=Abonnement exporteren +# Generic activity sorting +activity.generic.sorting=Automatisch verwerken +activity.generic.sorting.subtitle=Berichten automatisch verwerken met gebruik van een trefwoord (optioneel) +activity.generic.sort.header=Berichten automatisch verwerken met gebruik van een trefwoord (optioneel) +activity.generic.sort.description=Als mensen een bericht verzenden dat begint met een specifiek trefwoord, dan kan FrontlineSMS de berichten in uw systeem automatisch verwerken. +activity.generic.keywords.title=Trefwoorden invoeren voor activiteit. U kunt meerdere trefwoorden invoeren, gescheiden door komma's: +activity.generic.keywords.subtitle=Trefwoorden invoeren voor activiteit +activity.generic.keywords.info=U kunt meerdere trefwoorden invoeren, gescheiden door komma's: +activity.generic.no.keywords.title=Geen trefwoord gebruiken +activity.generic.no.keywords.description=Alle inkomende berichten die niet overeenkomen met andere trefwoorden zullen deze activiteit activeren +activity.generic.disable.sorting=De berichten niet automatisch sorteren +activity.generic.disable.sorting.description=In deze activiteit zullen de berichten niet automatisch gesorteerd worden +activity.generic.enable.sorting=De antwoorden met een trefwoord automatisch verwerken +activity.generic.sort.validation.unique.error=Trefwoorden moeten uniek zijn +activity.generic.keyword.in.use=Het trefwoord {0} is reeds in gebruik door activiteit{1} +activity.generic.global.keyword.in.use=Activiteit {0} is ingesteld om alle berichten te ontvangen die niet overeenkomen met andere trefwoorden. Met deze instelling kan slechts een enkele activiteit actief zijn +#basic authentication +auth.basic.label=Basis authenticatie +auth.basic.info=Een gebruikersnaam en wachtwoord vereisen om toegang te verkrijgen tot het gehele netwerk van FrontlineSMS +auth.basic.enabled.label=Basis Authenticatie Ondersteuning +auth.basic.username.label=Gebruikersnaam +auth.basic.password.label=Wachtwoord +auth.basic.confirmPassword.label=Wachtwoord bevestigen +auth.basic.password.mismatch=Wachtwoorden komen niet overeen +newfeatures.popup.title=Nieuwe Functies +newfeatures.popup.showinfuture=Deze dialoog voortaan tonen +dynamicfield.message_text.label=Bericht tekst +dynamicfield.message_text_with_keyword.label=Bericht tekst met trefwoord +dynamicfield.sender_name.label=Afzender Naam +dynamicfield.sender_number.label=Afzender Nummer +dynamicfield.recipient_number.label=Ontvanger Nummer +dynamicfield.recipient_name.label=Ontvanger Naam +# Smpp Fconnection +smpp.label=SMPP-account +smpp.type.label=Type +smpp.name.label=Naam +smpp.send.label=Om te verzenden +smpp.receive.label=Om te verzenden +smpp.url.label=SMSC URL +smpp.port.label=SMSC Port +smpp.username.label=Gebruikersnaam +smpp.password.label=Wachtwoord +smpp.fromNumber.label=Van Nummer +smpp.description=Berichten verzenden en ontvangen via een SMSC +smpp.global.info=You will need to get an account with your phone network of choice. +smpp.send.validator.invalid=You cannot configure a connection without SEND or RECEIVE fuctionality. +routing.title=Create rules for which phone number is used by outgoing messages. +routing.info=These rules will determine how the system selects which connection or phone number to use to send outgoing messages. Remember, the phone number seen by recipients may depend on the rules you set here. Also, changing this configuration may affect the cost of sending messages. +routing.rules.sending=When sending outgoing messages: +routing.rules.not_selected=If none of the above rules match: +routing.rules.otherwise=Anders: +routing.rules.device=Gebruik {0} +routing.rule.uselastreceiver=Send through most recent number that the contact messaged +routing.rule.useany=Gebruik een telefoonnummer dat beschikbaar is +routing.rule.dontsend=Dit bericht niet verzenden +routing.notification.no-available-route=Outgoing message(s) not sent due to your routing preferences. +routing.rules.none-selected.warning=Waarschuwing: U heeft geen regels of telefoonnummers geselecteerd. Geen bericht zal verzonden worden. ALs u een bericht wilt sturen dient u een verbinding tot stand te brengen. +customactivity.overview=Overzicht +customactivity.title={0} +customactivity.confirm=Bevestigen +customactivity.label=Aangepaste activiteitsopbouw +customactivity.description=Uw eigen activiteit opmaken door aangepaste instellingen te verbinden met een door u gekozen wachtwoord. +customactivity.name.prompt=Geef deze activiteit een naam +customactivity.moreactions.delete=Activiteit verwijderen +customactivity.moreactions.rename=Activiteit hernoemen +customactivity.moreactions.edit=Activiteit bewerken +customactivity.moreactions.export=Export activity +customactivity.text.none=Geen +customactivity.config=Configure +customactivity.config.description=Build and configure a set of actions for this activity. The actions will all be executed when a message matches the criteria you set on the previous step. +customactivity.info=Your Custom Activity has been created, and any messages containing your keyword will have the specified actions applied to it. +customactivity.info.warning=Without a keyword, all incoming messages will trigger the actions in this Custom Activity. +customactivity.info.note=Note: If you archive the Custom Activity, incoming messages will no longer be sorted for it. +customactivity.save.success={0} activity saved +customactivity.action.steps.label=Action Steps +validation.group.notnull=Selecteer een groep +customactivity.join.description=Aan een "{0}" groep deelnemen +customactivity.leave.description=Een "{0}" groep verlaten +customactivity.forward.description=Doorsturen met "{0}" +customactivity.webconnectionStep.description=Upload naar "{0}" +customactivity.reply.description=Beantwoorden met "{0}" +customactivity.step.join.add=Verzender toevoegen aan groep +customactivity.step.join.title=Verzender toevoegen aan groep* +customactivity.step.leave.add=Afzenders verwijderen van {0} +customactivity.step.leave.title=Afzenders verwijderen van {0} +customactivity.step.reply.add=Automatisch antwoord versturen +customactivity.step.reply.title=Bericht invoeren voor automatisch antwoord naar afzender* +customactivity.step.forward.add=Bericht doorsturen +customactivity.step.forward.title=Een bericht automatisch doorsturen naar een of meerdere contactpersonen +customactivity.manual.sorting=Automatisch verwerken uitgeschakeld +customactivity.step.webconnectionStep.add=Bericht uploaden naar een URL +customactivity.step.webconnectionStep.title=Bericht uploaden naar een URL +customactivity.validation.error.autoreplytext=Antwoord bericht is vereist +customactivity.validation.error.name=Url is verplicht +customactivity.validation.error.url=Url is verplicht +customactivity.validation.error.paramname=Parameter naam is vereist +recipientSelector.keepTyping=Keep typing... +recipientSelector.searching=Bezig met zoeken +validation.recipients.notnull=Please select at least one recipient +localhost.ip.placeholder=your-ip-address diff --git a/plugins/frontlinesms-core/grails-app/i18n/messages_pt.properties b/plugins/frontlinesms-core/grails-app/i18n/messages_pt.properties index dae963e3c..66eea9e6e 100644 --- a/plugins/frontlinesms-core/grails-app/i18n/messages_pt.properties +++ b/plugins/frontlinesms-core/grails-app/i18n/messages_pt.properties @@ -1,9 +1,7 @@ # FrontlineSMS English translation by the FrontlineSMS team, Nairobi language.name=Português - # General info app.version.label=Versão - # Common action imperatives - to be used for button labels and similar action.ok=Confirmar action.close=Fechar @@ -16,24 +14,20 @@ action.create=Criar action.edit=Editar action.rename=Renomear action.save=Salvar -action.save.all=Salvar Todos +action.save.all=Save Selected action.delete=Remover -action.delete.all=Remover Todos +action.delete.all=Delete Selected action.send=Enviar action.export=Exportar - # Messages when FrontlineSMS server connection is lost server.connection.fail.title=A Conexão com o servidor foi perdida. server.connection.fail.info=Por favor reinicie FrontlineSMS ou feche esta janela. - #Connections: connection.creation.failed=A conexão não pode ser criada {0} -connection.route.destroyed=Rota de {0} para {1} destruída. -connection.route.connecting=Conectando... -connection.route.disconnecting=Desconectando... +connection.route.disabled=Rota de {0} para {1} destruída. connection.route.successNotification=Rota em {0} criada com sucesso. -connection.route.failNotification=Falha ao criar rota em {1}: {2} [editar] -connection.route.destroyNotification=Rota em {0} desconectada. +connection.route.failNotification=Failed to create connection on {1}: {2} [[[edit]](({0}))]. +connection.route.disableNotification=Rota em {0} desconectada. connection.test.sent=Mensagem teste para {0} usando {1} enviada com sucesso. # Connection exception messages connection.error.org.smslib.alreadyconnectedexception=Dispositivo já conectado. @@ -41,16 +35,14 @@ connection.error.org.smslib.gsmnetworkregistrationexception=Falha ao registrar c connection.error.org.smslib.invalidpinexception=O PIN informado está incorreto. connection.error.org.smslib.nopinexception=PIN requerido mas não informado. connection.error.java.io.ioexception=A porta lançou um erro: {0} - -connection.header=Conexões +connection.header=Settings > Connections connection.list.none=Você não tem conexões configuradas. connection.edit=Editar Conexão connection.delete=Remover Conexão connection.deleted=Conexão {0} foi removida -connection.route.create=Criar rota connection.add=Adicionar nova conexão. connection.createtest.message.label=Mensagem -connection.route.destroy=Destruir rota +connection.route.disable=Destruir rota connection.send.test.message=Mandar mensagem de teste connection.test.message=Parabéns do FrontlineSMS \\o/ você configurou com sucesso {0} para enviar SMS \\o/ connection.validation.prompt=Por favor preencha todos os campos obrigatórios @@ -61,21 +53,17 @@ connection.confirm=Confirmar connection.createtest.number=Número connection.confirm.header=Confirmar configurações connection.name.autoconfigured=Auto-configuração {0} {1} na porta {2}" - -status.connection.header=Conexões status.connection.none=Você não tem conexões configuradas. status.devises.header=Detectar dispositivos status.detect.modems=Detectar Modems status.modems.none=Nenhum dispositivo encontrado até o momento. - connectionstatus.not_connected=Desconectado connectionstatus.connecting=Conectando connectionstatus.connected=Conectado - default.doesnt.match.message=A propriedade [{0}] da classe [{1}] com valor [{2}] não coincide com o padrão necessário [{3}] default.invalid.url.message=A propriedade [{0}] da classe [{1}] com valor [{2}] não é uma URL válida default.invalid.creditCard.message=A propriedade [{0}] da classe [{1}] com valor [{2}] não é um número de cartão de crédito válido -default.invalid.email.message=A propriedade [{0}] da classe [{1}] com valor [{2}] não é um endereço de email válido +default.invalid.email.message=A propriedade [{0}] da classe [{1}] com valor [{2}] não é um endereço de email válido default.invalid.range.message=A propriedade [{0}] da classe [{1}] com valor [{2}] não está no limite de valores de [{3}] a [{4}] default.invalid.size.message=A propriedade [{0}] da classe [{1}] com valor [{2}] não está no limite de tamanho entre [{3}] e [{4}] default.invalid.max.message=A propriedade [{0}] da classe [{1}] com valor [{2}] é maior que o máximo valor permitido [{3}] @@ -88,39 +76,33 @@ default.blank.message=A propriedade [{0}] da classe [{1}] não pode estar em bra default.not.equal.message=A propriedade [{0}] da classe [{1}] com valor [{2}] não pode ser igual a [{3}] default.null.message=A propriedade [{0}] da classe [{1}] não pode ser nula default.not.unique.message=A propriedade [{0}] da classe [{1}] com valor [{2}] deve ser única - default.paginate.prev=Anterior default.paginate.next=Próximo default.boolean.true=Verdadeiro default.boolean.false=Falso default.date.format=dd MMMM, yyyy hh:mm default.number.format=0 - -default.unarchived={0} desarquivar +default.unarchived={0} desarquivado default.unarchive.failed=Desarquivar {0} falhou -default.trashed={0} enviados para lixeira +default.trashed={0} moved to trash default.restored={0} restaurados default.restore.failed=Não foi possível restaurar {0} com id {1} -default.archived={0} arquivados com sucesso! +default.archived={0} archived default.archived.multiple={0} arquivados default.created={0} criado default.created.message={0} {1} foi criado default.create.failed=Falha ao criar {0} default.updated={0} foi atualizado default.update.failed=Falha ao atualizar {0} com id {1} -default.updated.multiple= {0} foram atualizadas +default.updated.multiple={0} foram atualizadas default.updated.message={0} atualizada default.deleted={0} removido -default.trashed={0} movido para a lixeira default.trashed.multiple={0} movido para a lixeira -default.archived={0} arquivado -default.unarchived={0} desarquivado default.unarchive.keyword.failed=Desarquivando {0} falhou. Palavra-chave já está em uso default.unarchived.multiple={0} desarquivados default.delete.failed=Não foi possível remover {0} com id {1} default.notfound=Não foi possível encontrar {0} com id {1} default.optimistic.locking.failure=Outro usuário atualizou este {0} enquanto você estava editando - default.home.label=Página Inicial default.list.label={0} Lista default.add.label=Adicionar {0} @@ -129,7 +111,6 @@ default.create.label=Criar {0} default.show.label=Mostrar {0} default.edit.label=Editar {0} search.clear=Limpar Busca - default.button.create.label=Criar default.button.edit.label=Editar default.button.update.label=Atualizar @@ -137,9 +118,7 @@ default.button.delete.label=Remover default.button.search.label=Buscar default.button.apply.label=Aplicar default.button.delete.confirm.message=Você tem certeza? - default.deleted.message={0} removida - # Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) typeMismatch.java.net.URL=Propriedade {0} precisa ser uma URL válida typeMismatch.java.net.URI=Propriedade {0} precisa ser uma URI válida @@ -150,8 +129,7 @@ typeMismatch.java.lang.Long=Propriedade {0} precisa ser um número válido typeMismatch.java.lang.Short=Propriedade {0} precisa ser um número válido typeMismatch.java.math.BigDecimal=Propriedade {0} precisa ser um número válido typeMismatch.java.math.BigInteger=Propriedade {0} precisa ser um número válido -typeMismatch.int = {0} precisa ser um número válido - +typeMismatch.int={0} precisa ser um número válido # Application specific messages messages.trash.confirmation=Isto irá esvaziar o lixeira e apagar as mensagens permanentemente. Tem certeza que você quer continuar? default.created.poll=A enquete foi criada! @@ -160,39 +138,6 @@ default.search.betweendates.title=Entre as datas: default.search.moresearchoption.label=Mais opções de busca default.search.date.format=d/M/yyyy default.search.moreoption.label=Mais opções - -# SMSLib Fconnection -smslibfconnection.label=Telefone/Modem -smslibfconnection.type.label=Tipo -smslibfconnection.name.label=Nome -smslibfconnection.port.label=Porta -smslibfconnection.baud.label=Taxa de Transmissão -smslibfconnection.pin.label=PIN -smslibfconnection.imsi.label=SIM IMSI -smslibfconnection.serial.label=# Serial do Dispositivo -smslibfconnection.send.label=Use o Modem APENAS para o Envio de Mensagens -smslibfconnection.receive.label =Use o Modem APENAS para o Recebimeto de Mensagens -smslibFconnection.send.validator.error.send=Modem deve ser usada para o envio -smslibFconnection.receive.validator.error.receive=ou recebimento de mensagens - -# Email Fconnection -emailfconnection.label=Email -emailfconnection.type.label=Tipo -emailfconnection.name.label=Nome -emailfconnection.receiveProtocol.label=Protocolo -emailfconnection.serverName.label=Nome do Servidor -emailfconnection.serverPort.label=Porta do Servidor -emailfconnection.username.label=Usuário -emailfconnection.password.label=Senha - -# CLickatell Fconnection -clickatellfconnection.label=Conta Clickatell -clickatellfconnection.type.label=Tipo -clickatellfconnection.name.label=Nome -clickatellfconnection.apiId.label=API ID -clickatellfconnection.username.label=Usuário -clickatellfconnection.password.label=Senha - # Messages Tab message.create.prompt=Digite a mensagem message.character.count=Caracteres restantes {0} ({1} Mensagem(ns) SMS) @@ -222,8 +167,6 @@ announcement.moreactions.rename=Renomear anúncio announcement.moreactions.edit=Editar anúncio announcement.moreactions.export=Exportar anúncio frontlinesms2.Announcement.name.unique.error.frontlinesms2.Announcement.name={1} {0} "{2}" deve ser único - - archive.inbox=Caixa de Entrada archive.sent=Enviados archive.activity=Atividade @@ -263,7 +206,6 @@ frontlinesms2.Autoreply.name.unique.error.frontlinesms2.Autoreply.name={1} {0} " frontlinesms2.Autoreply.name.validator.error.frontlinesms2.Autoreply.name=O nome da resposta automática deve ser único frontlinesms2.Keyword.value.validator.error.frontlinesms2.Autoreply.keyword.value=Palavra-chave "{2}" já está em uso frontlinesms2.Autoreply.autoreplyText.nullable.error.frontlinesms2.Autoreply.autoreplyText=Mensagem não pode ser vazia - contact.new=Novo contato contact.list.no.contact=Nenhum contato aqui! contact.header=Contatos @@ -278,22 +220,19 @@ contact.remove.from.group=Remover do grupo contact.customfield.addmoreinformation=Adicionar mais informações... contact.customfield.option.createnew=Criar novo... contact.name.label=Nome -contact.phonenumber.label=Celular -contact.phonenumber.international.warning=Este número não está no formato internacional. Isto pode causar problemas com as mensagens correspondentes aos contatos. +contact.phonenumber.label=Telefone contact.notes.label=Notas contact.email.label=Email contact.groups.label=Grupos contact.notinanygroup.label=Não é parte de nenhum grupo contact.messages.label=Mensagens -contact.sent.messages={0} mensagens enviadas +contact.messages.sent={0} mensagens enviadas contact.received.messages={0} mensagens recebidas contact.search.messages=Procurar mensagens - group.rename=Renomear grupo group.edit=Editar grupo group.delete=Remover grupo group.moreactions=Mais ações... - customfield.validation.prompt=Por favor preencha com um nome customfield.name.label=Nome export.contact.info=Para exportar contatos do FrontlineSMS escolha o tipo de exportação e a informação a ser incluída nos dados exportados. @@ -308,7 +247,7 @@ activities.header=Atividades activities.create=Criar nova atividade folder.header=Pastas folder.create=Criar nova pasta -folder.label=pasta +folder.label=Pasta message.folder.header={0} Pasta fmessage.trash.actions=Ações de lixeira... fmessage.trash.empty=Esvaziar lixeira @@ -323,9 +262,6 @@ poll.message.label=Mensagem poll.choice.validation.error.deleting.response=Uma escolha salva não pode ter um valor vazio poll.alias=Apelidos poll.aliases.prompt=Digite os apelidos para as opções correspondentes. -poll.aliases.prompt.details=Você pode inserir vários apelidos para cada opção, separados por vírgulas. O primeiro apelido será enviado na mensagem de instruções da enquete. -poll.alias.validation.error=Apelidos devem ser únicos - poll.sort.label=Ordenação automática poll.autosort.no.description=As mensagens não serão ordenadas automaticamente. poll.autosort.description=Ordenar mensagens com palavra-chave @@ -347,26 +283,22 @@ poll.response.enabled=Resposta automática habilitada poll.message.edit=Editar mensagem a ser enviada para destinatários poll.message.prompt=A seguinte mensagem será enviada para os destinatários da enquete poll.message.count=Caracteres restantes 160 (1 mensagem SMS) - poll.moreactions.delete=Remover enquete poll.moreactions.rename=Renomear enquete poll.moreactions.edit=Editar enquete poll.moreactions.export=Exportar enquete - #TODO embed javascript values -poll.reply.text=Responda "{0} {1}" para Sim, "{2} {3}" para Não. -poll.reply.text1={0} "{1} {2}" para {3} +poll.reply.text=Responda "{0}" para Sim, "{1}" para Não. +poll.reply.text1={0} "{1}" para {2} poll.reply.text2=Por favor, responda 'Sim' ou 'Não' -poll.reply.text3= ou -poll.reply.text4={0} {1} +poll.reply.text3=ou poll.reply.text5=Responder poll.reply.text6=Por favor, responda poll.message.send={0} {1} poll.recipients.validation.error=Escolha os contatos para enviar as mensagens -frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name = {1} {0} "{2}" devem ser únicos +frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name={1} {0} "{2}" devem ser únicos frontlinesms2.Poll.responses.validator.error.frontlinesms2.Poll.responses=Opções de resposta não podem ser idênticas -frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value = Palavra-chave "{2}" já é utilizada - +frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value=Palavra-chave "{2}" já é utilizada wizard.title.new=Novo wizard.fmessage.edit.title=Editar {0} popup.title.saved={0} salvo! @@ -387,14 +319,14 @@ smallpopup.messages.export.title=Exportar Resultados ({0} mensagens) smallpopup.test.message.title=Mensagem de Teste smallpopup.recipients.title=Destinatários smallpopup.folder.title=Pasta -smallpopup.group.title=Grupo +smallpopup.group.create.title=Group smallpopup.contact.export.title=Exportar smallpopup.contact.delete.title=Remover contact.selected.many={0} contatos selecionados group.join.reply.message=Bem vindo group.leave.reply.message=Até logo fmessage.new.info=Você tem {0} mensagens novas. Clique para ver -wizard.quickmessage.title=Mensagem Rápida +wizard.quickmessage.title=Send Message wizard.messages.replyall.title=Responder para todos wizard.send.message.title=Enviar Mensagem wizard.ok=Ok @@ -402,10 +334,8 @@ wizard.create=Criar wizard.send=Enviar common.settings=Configurações common.help=Ajuda - activity.validation.prompt=Por favor, preencha todos os campos obrigatórios autoreply.blank.keyword=Palavra-chave em branco. Uma resposta será enviado para todas as mensagens recebidas - poll.type.prompt=Selecione o tipo de enquete para criar poll.question.yes.no=Pergunta com um 'Sim' ou 'Não' como resposta poll.question.multiple=Questão de múltipla escolha (por exemplo, 'Vermelho', 'Azul', 'Verde') @@ -422,7 +352,6 @@ poll.sort.automatically=Ordenar mensagens automaticamente que têm a seguinte pa poll.validation.prompt=Por favor, preencha todos os campos obrigatórios poll.name.validator.error.name=Nome da Enquete deve ser único pollResponse.value.blank.value=Valor de resposta da enquete não pode estar em branco -poll.alias.validation.error.invalid.alias=Apelidos inválidos. Tente um nome ou palavra poll.question=Digite Pergunta poll.response=Lista de Respostas poll.sort=Classificação automática @@ -453,8 +382,7 @@ quickmessage.phonenumber.label=Adicionar número de telefone: quickmessage.phonenumber.add=Adicionar quickmessage.selected.recipients=destinatários selecionados quickmessage.validation.prompt=Por favor, preencha todos os campos obrigatórios - -fmessage.number.error=Caracteres neste campo serão removidos ao salvar +fmessage.number.error=Non-numeric characters in this field will be removed when saved search.filter.label=Limitar Busca a search.filter.group=Selecionar grupo search.filter.activities=Selecionar atividade/pasta @@ -464,7 +392,7 @@ search.filter.sent=Apenas mensagens enviadas search.filter.archive=Incluir Arquivo search.betweendates.label=Entre datas search.header=Busca -search.quickmessage=Mensagem rápida +search.quickmessage=Send message search.export=Exportar resultados search.keyword.label=Palavra-chave ou frase search.contact.name.label=Nome do contato @@ -475,29 +403,22 @@ settings.general=Geral settings.connections=Telefones & conexões settings.logs=Sistema settings.general.header=Ajustes > Geral -settings.logs.header=Logs do Sistema +settings.logs.header=Settings > System Logs logs.none=Você não possui logs. logs.content=Mensagem logs.date=Hora logs.filter.label=Exibir logs para logs.filter.anytime=qualquer hora -logs.filter.1day=últimas 24 horas -logs.filter.3days=últimos 3 dias -logs.filter.7days=últimos 7 dias -logs.filter.14days=últimos 14 dias -logs.filter.28days=últimos 28 dias logs.download.label=Baixar logs do sistema logs.download.buttontext=Baixar logs logs.download.title=Baixar logs para enviar logs.download.continue=Continuar - smartgroup.validation.prompt=Por favor preencha todos os campos obrigatórios. Você pode especificar apenas uma regra por campo. smartgroup.info=Para criar um Grupo Inteligente, selecione os critérios a serem satisfeitos pelos contatos deste grupo smartgroup.contains.label=contém smartgroup.startswith.label=começa com smartgroup.add.anotherrule=Adicionar outra regra smartgroup.name.label=Nome - modem.port=Porta modem.description=Descrição modem.locked=Travado? @@ -511,30 +432,12 @@ traffic.all.folders.activities=Exibir todas as atividades/pastas traffic.sent=Enviado traffic.received=Recebido traffic.total=Total - tab.message=Mensagens tab.archive=Arquivo tab.contact=Contatos tab.status=Status tab.search=Busca - help.info=Esta é uma versão beta, portanto não possui ajuda embutida. Por favor acesse os fóruns de ajuda ao usuário para obter auxílio neste estágio - -# IntelliSms Fconnection -intellismsfconnection.label=Conta IntelliSms -intellismsfconnection.type.label=Tipo -intellismsfconnection.name.label=Nome -intellismsfconnection.username.label=Usuário -intellismsfconnection.password.label=Senha - -intellismsfconnection.send.label=Use para envio -intellismsfconnection.receive.label=Use para recebimento -intellismsfconnection.receiveProtocol.label=Protocolo -intellismsfconnection.serverName.label=Nome do Servidor -intellismsfconnection.serverPort.label=Porta do Servidor -intellismsfconnection.emailUserName.label=Usuário -intellismsfconnection.emailPassword.label=Senha - #Controllers contact.label=Contato(s) contact.edited.by.another.user=Outro usuário atualizou este Contato enquanto você estava editando @@ -543,7 +446,6 @@ contact.exists.warn=Já existe um contato com este número contact.view.duplicate=Visualizar duplicados contact.addtogroup.error=Não é possível adicionar e remover do mesmo grupo! contact.mobile.label=Celular -contact.email.label=Email fconnection.label=Fconnection fconnection.name=Fconnection fconnection.unknown.type=Tipo de conexão desconhecida: @@ -551,7 +453,6 @@ fconnection.test.message.sent=Mensagem de teste enviada! announcement.saved=O anúncio foi salvo e a(s) mensagem(ns) foram colocadas na fila de envio announcement.not.saved=O anúncio não pôde ser salvo! announcement.id.exist.not=Anúncio com id {0} não encontrado -autoreply.saved=Resposta automática salva! autoreply.not.saved=Resposta automática não pôde ser salva! report.creation.error=Erro na criação do relatório export.message.title=Exportação de Mensagem FrontlineSMS @@ -573,36 +474,28 @@ export.messages.name2={0} ({1} mensagens) export.contacts.name1={0} grupo ({1} contatos) export.contacts.name2={0} grupo inteligente ({1} contatos) export.contacts.name3=Todos os contatos ({0} contatos) -folder.label=Pasta folder.archived.successfully=Pasta arquivada com sucesso! folder.unarchived.successfully=Pasta desarquivada com sucesso! folder.trashed=Pasta removida! folder.restored=Pasta restaurada! folder.exist.not=Pasta com id {0} não encontrada folder.renamed=Pasta Renomeada - group.label=Grupo group.name.label=Nome group.update.success=Grupo atualizado com sucesso group.save.fail=Falha ao salvar grupo group.delete.fail=Não é possível remover o grupo - -import.label=Importar -import.backup.label=Importar dados de backup anterior -import.prompt.type=Selecione o tipo de dados para importar -import.contacts=Detalhes do contato -import.messages=Detalhes da mensagem -import.version1.info=Para importar dados da versão 1, por favor exporte-os em Inglês -import.prompt=Selecione um arquivo de dados para importar +import.label=Import contacts and messages +import.backup.label=You can use the form below to import your contacts. You can also import messages that you have from a previous Frontline project. +import.prompt.type=Select the type of data you wish to import before uploading +import.messages=Messages in Frontline's CSV format +import.prompt=Select the file containing your data to initiate the import import.upload.failed=O carregamento do arquivo falhou por algum motivo. import.contact.save.error=Encontrado erro ao salvar contato import.contact.complete={0} contatos foram importados; {1} falharam -import.contact.failed.download=Baixar contatos com falha (CSV) import.message.save.error=Erro encontrado ao salvar mensagem import.message.complete={0} mensagens foram importadas; {1} falharam - -many.selected = {0} {1}s selecionados(as) - +many.selected={0} {1}s selecionados(as) flash.message.activity.found.not=Atividade não encontrada flash.message.folder.found.not=Pasta não encontrada flash.message=Mensagem @@ -611,45 +504,38 @@ flash.message.fmessages.many={0} mensagens SMS flash.message.fmessages.many.one=1 mensagem SMS fmessage.exist.not=Mensagem com id {0} não encontrada flash.message.poll.queued=Enquete salva e mensagem(ns) colocadas na fila de envio -flash.message.poll.saved=Enquete salva flash.message.poll.not.saved=A enquete não pôde ser salva! -system.notification.ok=OK +system.notification.ok=Confirmar system.notification.fail=FALHA flash.smartgroup.delete.unable=Não foi possível remover grupo inteligente flash.smartgroup.saved=Grupo inteligente {0} salvo flash.smartgroup.save.failed=Falha ao salvar grupo inteligente. Erros foram {0} smartgroup.id.exist.not=Grupo inteligente com id {0} não encontrado smartgroup.save.failed=Falha ao salvar grupo inteligente{0}com parâmetros {1}{2}erros: {3} -contact.name.label=Nome -contact.phonenumber.label=Telefone - searchdescriptor.searching=Buscando -searchdescriptor.all.messages= todas as mensagens +searchdescriptor.all.messages=todas as mensagens searchdescriptor.archived.messages=, incluindo mensagens arquivadas searchdescriptor.exclude.archived.messages=, sem mensagens arquivadas searchdescriptor.only=, apenas {0} searchdescriptor.between=, entre {0} e {1} searchdescriptor.from=, a partir de {0} searchdescriptor.until=, até {0} -poll.title={0} enquete -announcement.title={0} anúncio -autoreply.title={0} resposta automática -folder.title={0} pasta +poll.title={0} +announcement.title={0} +autoreply.title={0} +folder.title={0} frontlinesms.welcome=Bem-vindo ao FrontlineSMS! \\o/ failed.pending.fmessages={0} mensagem(ns) pendente(s) com falha. Vá à seção de mensagens pendentes para visualizar. - language.label=Idioma language.prompt=Escolha o idioma da interface do FrontlineSMS frontlinesms.user.support=Ajuda ao Usuário do FrontlineSMS download.logs.info1=ATENÇÃO: O time FrontlineSMS não consegue responder diretamente aos logs enviados. Se você tem uma requisição de ajuda ao usuário, por favor verifique os arquivos de Ajuda para ver se é possível encontrar a resposta lá. Caso contrário, envie seu problema pelos nossos fóruns de ajuda ao usuário: download.logs.info2=Outros usuários podem ter relatado o mesmo problema e encontrado uma solução! Para continuar com o envio dos seus logs, por favor clique em 'Continuar' - dynamicfield.contact_name.label=Nome do Contato dynamicfield.contact_number.label=Número do Contato dynamicfield.keyword.label=Palavra-chave dynamicfield.message_content.label=Conteúdo da Mensagem - -# Fmessage domain +# TextMessage domain fmessage.queued=Mensagem adicionada à fila para ser enviada para {0} fmessage.queued.multiple=Mensagem adicionada à fila para enviar para {0} destinatários fmessage.retry.success=Mensagem re-adicionada à fila para ser enviada para {0} @@ -659,14 +545,14 @@ fmessage.text.label=Mensagem fmessage.date.label=Data fmessage.to=Para: {0} fmessage.to.multiple=Para: {0} destinatários -fmessage.quickmessage=Mensagem rápida +fmessage.quickmessage=Send message fmessage.archive=Arquivar fmessage.activity.archive=Arquivar {0} -fmessage.unarchive=Desarquivar {0} +fmessage.unarchive=Desarquivar fmessage.export=Exportar fmessage.rename=Renomear {0} fmessage.edit=Editar {0} -fmessage.delete=Remover {0} +fmessage.delete=Delete fmessage.moreactions=Mais ações... fmessage.footer.show=Visualizar fmessage.footer.show.failed=Com Falha @@ -685,37 +571,31 @@ fmessage.resend=Reenviar fmessage.retry=Tentar Novamente fmessage.reply=Responder fmessage.forward=Encaminhar -fmessage.unarchive=Desarquivar -fmessage.delete=Remover fmessage.messages.none=Nenhuma mensagem aqui! fmessage.selected.none=Nenhuma mensagem selecionada fmessage.move.to.header=Mover mensagem para... fmessage.move.to.inbox=Caixa de Entrada -fmessage.archive.many=Arquivar todas +fmessage.archive.many=Archive selected fmessage.count=1 mensagem fmessage.count.many={0} mensagens -fmessage.many= mensagens -fmessage.delete.many=Remover todas -fmessage.reply.many=Responder todas +fmessage.many=mensagens +fmessage.delete.many=Delete selected +fmessage.reply.many=Reply selected fmessage.restore=Restaurar fmessage.restore.many=Restaurar -fmessage.retry.many=Falha ao restaurar +fmessage.retry.many=Retry selected fmessage.selected.many={0} mensagens selecionadas -fmessage.unarchive.many=Desarquivar todas - +fmessage.unarchive.many=Unarchive selected # TODO move to poll.* fmessage.showpolldetails=Exibir grafo fmessage.hidepolldetails=Esconder grafo - # TODO move to search.* fmessage.search.none=Nenhuma mensagem encontrada fmessage.search.description=Inicie nova busca à esquerda - activity.name=Nome activity.delete.prompt=Mover {0} para lixeira. Isso moverá todas as mensagens associadas para a seção da lixeira. activity.label=Atividade activity.categorize=Categorizar resposta - magicwand.title=Adicionar expressões de substituição folder.create.success=Pasta criada com sucesso folder.create.failed=Não foi possível criar pasta @@ -729,7 +609,6 @@ announcement.name.blank.error=Nome do anúncio não pode estar em branco announcement.name.validator.error=Nome do anúncio já usado group.name.blank.error=Nome do grupo não pode estar em branco group.name.validator.error=Nome do grupo já existe - #Jquery Validation messages jquery.validation.required=Este campo é obrigatório jquery.validation.remote=Por favor, corrija este campo. @@ -748,4 +627,3 @@ jquery.validation.rangelength=Por favor, insira um valor entre {0} e {1} caracte jquery.validation.range=Por favor, insira um valor entre {0} e {1}. jquery.validation.max=Por favor, insira um valor inferior ou igual a {0}. jquery.validation.min=Por favor, entre com um valor maior ou igual a {0}. - diff --git a/plugins/frontlinesms-core/grails-app/i18n/messages_ru.properties b/plugins/frontlinesms-core/grails-app/i18n/messages_ru.properties index 4d4d290f2..21c554ab1 100644 --- a/plugins/frontlinesms-core/grails-app/i18n/messages_ru.properties +++ b/plugins/frontlinesms-core/grails-app/i18n/messages_ru.properties @@ -1,9 +1,7 @@ # FrontlineSMS English translation by the FrontlineSMS team, Nairobi language.name=Русский - # General info app.version.label=Версия - # Common action imperatives - to be used for button labels and similar action.ok=ok action.close=Закрыть @@ -16,43 +14,52 @@ action.create=Создать action.edit=Редактировать action.rename=Переименовать action.save=Сохранить -action.save.all=Сохранить все +action.save.all=Save Selected action.delete=Удалить -action.delete.all=Удалить все +action.delete.all=Delete Selected action.send=Оправить action.export=Экспортировать - +action.view=View +content.loading=Loading... # Messages when FrontlineSMS server connection is lost server.connection.fail.title=Соединение с сервером было потеряно. server.connection.fail.info=Пожалуйста, перезагрузите ФронтлайнСМС или закройте это окно. - #Connections: connection.creation.failed=Связь не может быть установлена {0} -connection.route.destroyed=Удалён маршрут от {0} до {1} -connection.route.connecting=Идёт соединение... -connection.route.disconnecting=Идёт отключение... +connection.route.disabled=Удалён маршрут от {0} до {1} connection.route.successNotification=Успешно создан маршрут на {0} -connection.route.failNotification=Не удалось создать маршрут на {1}: {2} [edit] -connection.route.destroyNotification=Удалён маршрут на {0} +connection.route.failNotification=Failed to create connection on {1}: {2} [[[edit]](({0}))]. +connection.route.disableNotification=Удалён маршрут на {0} +connection.route.pauseNotification=Paused connection on {0} +connection.route.resumeNotification=Resumed connection on {0} connection.test.sent=Тестовое сообщение успешно отправлено {0} используя {1} +connection.route.exception={1} # Connection exception messages connection.error.org.smslib.alreadyconnectedexception=Устройство уже подключено connection.error.org.smslib.gsmnetworkregistrationexception=Не удалось соедениться с сетью GSM connection.error.org.smslib.invalidpinexception=Неправильный PIN код connection.error.org.smslib.nopinexception=Необходимо ввести PIN код +connection.error.org.smslib.notconnectedexception={0} +connection.error.org.smslib.nosuchportexception=Порт не найдён, или не доступны connection.error.java.io.ioexception=Ошибка ввода/вывода: {0} - -connection.header=Связи +connection.error.frontlinesms2.camel.exception.invalidapiidexception={0} +connection.error.frontlinesms2.camel.exception.authenticationexception={0} +connection.error.frontlinesms2.camel.exception.insufficientcreditexception={0} +connection.error.serial.nosuchportexception=Порт не может быть найден +connection.error.org.apache.camel.runtimecamelexception=Не удается подключиться к связи +connection.error.onsave={0} +connection.header=Settings > Connections connection.list.none=Соединение отсутствует. connection.edit=Редактировать настройки соединения connection.delete=Прервать соединение connection.deleted=Соединение было удалено -connection.route.create=Создать маршрут +connection.route.enable=Enable +connection.route.retryconnection=Попробовать снова connection.add=Создать новое соединение connection.createtest.message.label=Тестовое сообщение -connection.route.destroy=Удалить маршрут +connection.route.disable=Удалить маршрут connection.send.test.message=Отправить тестовое сообщение -connection.test.message=Поздравление от ФтонрлайнСМС \\o/ вы успешно настроили {0} для отправки СМС \\o/ +connection.test.message=Поздравление от ФтонрлайнСМС \\o/ вы успешно настроили {0} для отправки СМС \\o/ connection.validation.prompt=Пожалуйста, заполните все обязательные поля connection.select=Выберите тип соединения connection.type=Выбрать тип @@ -61,17 +68,18 @@ connection.confirm=Подтвердить connection.createtest.number=Номер connection.confirm.header=Подтвердить настройки connection.name.autoconfigured=Авто-настроено {0} {1} на порт {2}" - -status.connection.header=Связи +status.connection.title=Связи +status.connection.manage=Manage your connections status.connection.none=У вас нет настроенного соединения. status.devises.header=Обнаруженные устройства status.detect.modems=Обнаружения Модемов status.modems.none=Устройство не обнаружено - -connectionstatus.not_connected=Не подключен +status.header=Usage Statistics connectionstatus.connecting=Подключается connectionstatus.connected=Подключено - +connectionstatus.disabled=Disabled +connectionstatus.failed=Не удалось +connectionstatus.not_connected=Не подключен default.doesnt.match.message=Свойство [{0}] класса [{1}] со значением [{2}] не соответствует требуемому образцу [{3}] default.invalid.url.message=Свойство [{0}] класса [{1}] со значением [{2}] не является допустимым URL default.invalid.creditCard.message=Свойство [{0}] класса [{1}] со значением [{2}] не является действительным номером кредитной карточки @@ -88,39 +96,33 @@ default.blank.message=Свойство [{0}] класса [{1}] не может default.not.equal.message=Свойство [{0}] класса [{1}] со значением [{2}] не может выть равным [{3}] default.null.message=Свойство [{0}] класса [{1}] не может быть нулевой default.not.unique.message=Свойство [{0}] класса [{1}] со значением [{2}] должен быть уникальным - default.paginate.prev=Предыдущий default.paginate.next=Следующий default.boolean.true=Корректный default.boolean.false=Ложный default.date.format=dd MMMM, yyyy hh:mm default.number.format=0 - default.unarchived={0} не архивирован default.unarchive.failed=разархивирование не удалось {0} -default.trashed={0} Удалено default.restored={0} востановленно default.restore.failed=Не удалось восстановить {0} с идентификатором {1} -default.archived={0} архивация прошла успешно! default.archived.multiple={0} заархивировано default.created={0} созданный default.created.message={0} {1} было создано default.create.failed=Не удалось создать{0} default.updated={0} был обновлен default.update.failed=не удалось обновить {0} с идентификатором {1} -default.updated.multiple= {0} были обновлены +default.updated.multiple={0} были обновлены default.updated.message={0} обновлен default.deleted={0} удаленный -default.trashed={0} перемещен в корзину +default.trashed={0} moved to trash default.trashed.multiple={0} перемещены в корзину -default.archived={0} заархивирован -default.unarchived={0} не заархивирован +default.archived={0} archived default.unarchive.keyword.failed=Архивация не удалась{0}. Ключевое слово уже используеться default.unarchived.multiple={0} не заархивирован default.delete.failed=Невозможно удалить {0} с идентификатором {1} default.notfound=Не найдено {0} с идентификатором {1} default.optimistic.locking.failure=Другой пользователь обновил этот {0} пока вы редактировали - default.home.label=Дом default.list.label={0} Лист default.add.label=Добавить{0} @@ -129,7 +131,6 @@ default.create.label=Создать {0} default.show.label=Показать {0} default.edit.label=Редактировать {0} search.clear=Очистить поиск - default.button.create.label=Создать default.button.edit.label=Редактировать default.button.update.label=Обновить @@ -137,9 +138,7 @@ default.button.delete.label=Удалить default.button.search.label=Искать default.button.apply.label=Применить default.button.delete.confirm.message=Вы уверены? - default.deleted.message={0} удалено - # Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) typeMismatch.java.net.URL=Свойство {0} должен быть действительный URL typeMismatch.java.net.URI=Свойство {0} должен быть действительный URL @@ -150,8 +149,7 @@ typeMismatch.java.lang.Long=Свойство {0} должно быть дейс typeMismatch.java.lang.Short=Свойство {0} должно быть действительным номером typeMismatch.java.math.BigDecimal=Свойство {0} должно быть действительным номером typeMismatch.java.math.BigInteger=Свойство {0} должно быть действительным номером -typeMismatch.int = {0} номер должен быть действительным - +typeMismatch.int={0} номер должен быть действительным # Application specific messages messages.trash.confirmation=Это очистит корзину и удалит сообщения навсегда. Вы хотите продолжить? default.created.poll=Опрос был создан! @@ -160,43 +158,83 @@ default.search.betweendates.title=Между датами: default.search.moresearchoption.label=Дополнительне параметры поиска default.search.date.format=д/М/гггг default.search.moreoption.label=Дополнительные варианты - # SMSLib Fconnection -smslibfconnection.label=Телефон/Модем -smslibfconnection.type.label=Тип -smslibfconnection.name.label=Название -smslibfconnection.port.label=Порт -smslibfconnection.baud.label=Скорость передачи данных -smslibfconnection.pin.label=PIN -smslibfconnection.imsi.label=SIM IMSI -smslibfconnection.serial.label=Серия устройства # -smslibfconnection.send.label=Использовать модем ТОЛЬКО для отправки сообщений -smslibfconnection.receive.label =Использовать модем ТОЛЬКО для приёма сообщений -smslibFconnection.send.validator.error.send= Модем должен быть сконфигурирован для отправки -smslibFconnection.receive.validator.error.receive=или для получения сообщений - +smslib.label=Телефон/Модем +smslib.type.label=Тип +smslib.name.label=Название +smslib.manufacturer.label=Производитель +smslib.model.label=Модель +smslib.port.label=Порт +smslib.baud.label=Скорость передачи данных +smslib.pin.label=PIN +smslib.imsi.label=SIM IMSI +smslib.serial.label=Серия устройства # +smslib.sendEnabled.label=Use for sending +smslib.receiveEnabled.label=Use for receiving +smslibFconnection.sendEnabled.validator.error.send=Modem should be used for sending +smslibFconnection.receiveEnabled.validator.error.receive=or receiving messages +smslib.description=Подключения к USB, последовательный порт и Bluetooth модемы и телефоны +smslib.global.info=ФтонрлайнСМС попытается автоматически настроить любой подключенный модем или телефон, но вы можете вручную настроить их здесь # Email Fconnection -emailfconnection.label=Электронная почта -emailfconnection.type.label=Тип -emailfconnection.name.label=Название -emailfconnection.receiveProtocol.label=Протокол -emailfconnection.serverName.label=Имя сервера -emailfconnection.serverPort.label=Порт Сервера -emailfconnection.username.label=Имя пользователя -emailfconnection.password.label=Пароль - +email.label=Электронная почта +email.type.label=Тип +email.name.label=Название +email.receiveProtocol.label=Протокол +email.serverName.label=Имя сервера +email.serverPort.label=Порт Сервера +email.username.label=Имя пользователя +email.password.label=Пароль # CLickatell Fconnection -clickatellfconnection.label=Акаунт в Clickatell -clickatellfconnection.type.label=Тип -clickatellfconnection.name.label=Название -clickatellfconnection.apiId.label=Идентификатор -clickatellfconnection.username.label=Имя пользователя -clickatellfconnection.password.label=Пароль - +clickatell.label=Акаунт в Clickatell +clickatell.type.label=Тип +clickatell.name.label=Название +clickatell.apiId.label=Идентификатор +clickatell.username.label=Имя пользователя +clickatell.password.label=Пароль +clickatell.sendToUsa.label=Отправить в США +clickatell.fromNumber.label=От число +clickatell.description=Отправлять и получать сообщения через аккаунт Clickatell +clickatell.global.info=Вам нужно будет настроить аккаунт с Clickatell (www.clickatell.com). +clickatellFconnection.fromNumber.validator.invalid=B США 'Номер Oт' необходим для отправки сообщений +# TODO: Change markup below to markdown +clickatell.info-local=In order to set up a Clickatell connection, you must first have a Clickatell account. If you do not have one, please go to the Clickatell site and register for a 'Developer's Central Account'. It is free to sign up for test messages, and the process should take less that 5 minutes.

Once you have an active Clickatell account, you will need to 'Create a Connection (API ID)' from the front page. First, select 'APIs,' then select 'Set up a new API.' From there, choose 'add HTTP API' with the default settings, then enter the relevant details below.

The 'Name' field is just for your own reference for your Frontline account, and not related to the Clickatell API, e.g. 'My local message connection'. +clickatell.info-clickatell=The following details should be copied and pasted directly from the Clickatell HTTP API screen. +#Nexmo Fconnection +nexmo.label=Nexmo +nexmo.type.label=Nexmo connection +nexmo.name.label=Название +nexmo.api_key.label=ключ API +nexmo.api_secret.label=API secret +nexmo.fromNumber.label=From number +nexmo.description=Send and receive messages through a Nexmo account. +nexmo.receiveEnabled.label=Receiving enabled +nexmo.sendEnabled.label=Sending enabled +# Smssync Fconnection +smssync.label=СМС Синхронизация +smssync.name.label=Название +smssync.type.label=Тип +smssync.receiveEnabled.label=Приём овеспечен +smssync.sendEnabled.label=Oтправление овеспечено +smssync.secret.label=Засекретить +smssync.timeout.label=Timeout (mins) +smssync.description=Используйте телефон Android с установлено приложение СМС синхронизации для отправки и получения СМС с ФтонрлайнСМС +smssync.field.secret.info=Засекрет эту часть программе +smssync.global.info=Скачай программю СМС синхронизации от smssync.ushahidi.com +smssync.timeout=The Android phone associated with "{0}" has not contacted your Frontline account for {1} minute(s) [edit] +smssync.info-setup=Frontline products enable you to send and receive messages through your Android phone. In order to do this you will need to:\n\n1. Input a 'Secret' and name your connection. A secret is simply a password of your choice.\n2. Download and install [SMSSync from the Android App store](https://play.google.com/store/apps/details?id=org.addhen.smssync&hl=en) to your Android phone\n3. Once you have created this connection, you can create a new Sync URL within SMSSync on your Android phone by entering the connection URL (generated by your Frontline product and displayed on the next page) and your chosen secret. See [The SMSSync Site](http://smssync.ushahidi.com/howto) for more help. +smssync.info-timeout=If SMSSync does not contact your Frontline product for a certain duration (default 60 minutes), your queued messages will NOT be sent, and you will see a notification that the messages failed to send. Select this duration below: +smssync.info-name=Finally, you should name your SMSSync connection with a name of your choice, e.g. 'Bob's work Android'. # Messages Tab -message.create.prompt= Введите текст сообщения +message.create.prompt=Введите текст сообщения message.character.count=Осталось символов {0} ({1} СМС сообщение (ия)) message.character.count.warning=Может быть увеличена после проведения изменений +message.header.inbox=Входящие +message.header.sent=Посланный +message.header.pending=В ожидании +message.header.trash=Мусор +message.header.folder=Папка +message.header.activityList=Список Деятельность +message.header.folderList=Список Папок announcement.label=Объявление announcement.description=Отправить объявление и упорядочить ответы announcement.info1=Объявление было сохранено и ответы были добавлены в очередь ожидаемых сообщений. @@ -222,8 +260,6 @@ announcement.moreactions.rename=Переименовать объявление announcement.moreactions.edit=Редактировать объявление announcement.moreactions.export=Экспортировать объявление frontlinesms2.Announcement.name.unique.error.frontlinesms2.Announcement.name={1} {0} "{2}" должен быть уникальным - - archive.inbox=Заархивировать ящик archive.sent=Отправить архив archive.activity=Статус архива @@ -237,8 +273,11 @@ archive.activity.type=Тип archive.activity.date=Дата archive.activity.messages=Сообщения archive.activity.list.none=  Нет архивированных действий +archive.header=Архив autoreply.enter.keyword=Введите ключевое слово autoreply.create.message=Введите текст сообщения +activity.autoreply.sort.description=Если люди посылают в сообщениях начиная с определенного ключевого слова, ФтонрлайнСМС может автоматически обрабатывать сообщения в вашей системе. +activity.autoreply.disable.sorting.description=Сообщения не будут автоматически перемещаются в эту деятельность и получать ответы autoreply.confirm=Подтвердите autoreply.name.label=Сообщение autoreply.details.label=Подтвердите детали @@ -248,9 +287,9 @@ autoreply.description=Автоматически ответить на вход autoreply.info=Автоматический ответ был создан, любые соощбения содержащие ключевое слово будут добавлены в Аутоответчик, который можно просмотреть, нажав на нее в правом меню. autoreply.info.warning=Автоответчик без ключевого слова будет отвечать на все входящие сообщения autoreply.info.note=Примечание: Если вы заархивируете Автоответчик, то входяшие сообщения не будут отсортированы. -autoreply.validation.prompt= Пожалуйста, заполните все необходимые поля. -autoreply.message.title= Сообщение будет отправлено на этот автоответчик: -autoreply.keyword.title= Автоматичски сортировать сообщения по ключевым словам: +autoreply.validation.prompt=Пожалуйста, заполните все необходимые поля. +autoreply.message.title=Сообщение будет отправлено на этот автоответчик: +autoreply.keyword.title=Автоматичски сортировать сообщения по ключевым словам: autoreply.name.prompt=Имя автоответчика autoreply.message.count=0 символов (1 СМС сообщение) autoreply.moreactions.delete=Удалить автоответчик @@ -263,10 +302,34 @@ frontlinesms2.Autoreply.name.unique.error.frontlinesms2.Autoreply.name={1} {0} " frontlinesms2.Autoreply.name.validator.error.frontlinesms2.Autoreply.name=Навание автоответчика должно быть уникальным frontlinesms2.Keyword.value.validator.error.frontlinesms2.Autoreply.keyword.value=Ключевое слово Keyword "{2}" уже используется frontlinesms2.Autoreply.autoreplyText.nullable.error.frontlinesms2.Autoreply.autoreplyText=Сообщение не может быть пустым - +autoforward.title={0} +autoforward.label=Aвтоматическая переадресация +autoforward.description=Автоматической переадресации входящих сообщений контактам +autoforward.recipientcount.current=Tекущей {0} получатель +autoforward.create.message=Введите сообщение +autoforward.confirm=Подтвердите +autoforward.recipients=Получатели +autoforward.name.prompt=Даете название автоматическаму сообщению +autoforward.details.label=Подтвердите детали +autoforward.keyword.label=Ключевое слово(а) +autoforward.name.label=Cообщение +autoforward.contacts=Kонтакты +autoforward.groups=Группы +autoforward.info=Автоматическая переадресация была создана, любые сообщения, содержащие ключевое слово будет добавлено в эту автоматическую пересылку деятельности, которые можно просмотреть, нажав на нее в правом меню руку. +autoforward.info.warning=Автоматическая переадресация без ключевого слова приведет к все входящие сообщения направляются +autoforward.info.note=TODO:Note: If you archive the Autoforward, incoming messages will no longer be sorted for it. +autoforward.save=Автоматическая пересылка была спасена! +autoforward.save.success={0} Аftomatichiskoye saabshenye saxtoneno!) Автоматическая пересылка была спасена! +autoforward.global.keyword=Hичто (все входящие сообщения будут обрабатываться) +autoforward.disabled.keyword=Hичто (автоматическая сортировка выключена) +autoforward.keyword.none.generic=Hичто +autoforward.groups.none=Hичто +autoforward.contacts.none=Hичто +autoforward.message.format=Cообщение contact.new=Новый контакт contact.list.no.contact=Здесь контактов нет! contact.header=Контакты +contact.header.group=Kонтакты >> {0} contact.all.contacts=Все контакты contact.create=Создать новый контакт contact.groups.header=Группы @@ -279,22 +342,29 @@ contact.customfield.addmoreinformation=Добавить дополнительн contact.customfield.option.createnew=Создать новую... contact.name.label=Название contact.phonenumber.label=Мобильный -contact.phonenumber.international.warning=Этот номер не в международном формате. Это может вызвать ошибку при сопоставлении сообщений к контактам. contact.notes.label=Записи contact.email.label=Электронная почта contact.groups.label=Группы contact.notinanygroup.label=Не принадлежит никакой группе contact.messages.label=Сообещения -contact.sent.messages={0} сообщения отравлены +contact.messages.sent={0} сообщение отравлено contact.received.messages={0} сообщения приняты contact.search.messages=Поиск сообщений - +contact.select.all=Выбрать все опции +contact.search.placeholder=Search your contacts, or enter phone numbers +contact.search.contact=Kонтакты +contact.search.smartgroup=Смарт - группы +contact.search.group=Группы +contact.search.address=Добавьте номер телефона: +contact.not.found=Контакты не найдены +group.not.found=Группа не найдена +smartgroup.not.found=Yмная группа не найдена group.rename=Переименовать группу group.edit=Редактировать группу group.delete=Удалить группу group.moreactions=Дополнительные действия... - customfield.validation.prompt=Пожалуйста, введите имя +customfield.validation.error=Имя уже существует customfield.name.label=Имя export.contact.info=Чтобы экспортировать контакты из ФронтлайнСМС, выберите тип экспорта и информацию которая должна быть экспортирована. export.message.info=Чтобы экспортировать сообщения из ФронтлайнСМС, выберите тип экспорта и иформацию которая должна быть экспортирована. @@ -308,7 +378,7 @@ activities.header=Событие activities.create=Создать новое событие folder.header=Папки folder.create=Создать новую папку -folder.label=папка +folder.label=Папка message.folder.header={0} Папка fmessage.trash.actions=Свойства корзины fmessage.trash.empty=Очистить корзину @@ -322,50 +392,64 @@ poll.details.label=Подтвердите детали poll.message.label=Сообещение poll.choice.validation.error.deleting.response=Сохраненный выбор не может иметь пустое значение poll.alias=Псевдонимы +poll.keywords=Kлючевые слова poll.aliases.prompt=Введите псевдонимы для соответствующих вариантов. -poll.aliases.prompt.details=Можно ввести несколько псевдонимов для каждого варианта, разделенные запятыми. Первый псевдоним будет отправлен в сообщении с инструкцией опроса. -poll.alias.validation.error=Псевдонимы должны быть уникальными +poll.keywords.prompt.details=Верхнего уровня ключевого слова назовешь опрос и направляйтесь в сообщении опроса инструкции. Каждый ответ также может иметь альтернативные короткие ключевые слова разреза. +poll.keywords.prompt.more.details=Вы можете ввести несколько ключевых слов, разделенных запятыми для верхнего уровня и ответов. Если нет верхнего уровня ключевые слова не будут введены ниже, то это ответ ключевые слова должны быть уникальны по всем видам деятельности. +poll.keywords.response.label=Ключевые слова +poll.response.keyword=Устанавливает реакцию ключевые слова +poll.set.keyword=Установить верхнего уровня ключевого слова +poll.keywords.validation.error=Ключевые слова должны быть уникальными poll.sort.label=Автоматическая сортировка poll.autosort.no.description=Сообщения не будут сортированы автоматически. poll.autosort.description=Сортировать сообщения по ключевым словам. poll.sort.keyword=ключевое слово +poll.sort.toplevel.keyword.label=Верхнего уровня ключевое слово (а) (опционально) poll.sort.by=Сортировать по poll.autoreply.label=Автоответчик poll.autoreply.none=Ничего poll.recipients.label=Получатели poll.recipients.none=Никого +poll.toplevelkeyword=Верхнего уровня ключевых слов +poll.sort.example.toplevel=например КОМАНДА +poll.sort.example.keywords.A=например A, Поразителяный +poll.sort.example.keywords.B=например B, Красивый +poll.sort.example.keywords.C=например C, Смелый +poll.sort.example.keywords.D=например D, Восхитительный +poll.sort.example.keywords.E=например E, Образцовый +poll.sort.example.keywords.yn.A=например ДА +poll.sort.example.keywords.yn.B=например Нет #TODO embed javascript values poll.recipients.count=выделенные контакты poll.messages.count=сообщения будут отравлены poll.yes=Да poll.no=Нет poll.label=Опрос -poll.description=Оправить сообщение и анализировать ответы +poll.description=Оправить сообщение и анализироNo, NOPвать ответы poll.messages.sent={0} сообщение отравлено poll.response.enabled=Автоответчик активирован poll.message.edit=Редактировать сообщение для отправки получателям poll.message.prompt=Сообщение будет отправлено получателям опроса poll.message.count=Осталось символов 160 (1 СМС сообщение) - poll.moreactions.delete=Удалить опрос poll.moreactions.rename=Переименовать опрос poll.moreactions.edit=Редактировать опрос poll.moreactions.export=Экспортировать опрос - +folder.moreactions.delete=Удалить директорию +folder.moreactions.rename=Переминовать директорию +folder.moreactions.export=Перислать директорию #TODO embed javascript values -poll.reply.text=Ответ "{0} {1}" на Да , "{2} {3}" на Нет. -poll.reply.text1={0} "{1} {2}" для {3} +poll.reply.text=Ответ "{0}" на Да , "{1}" на Нет. +poll.reply.text1={0} "{1}" для {2} poll.reply.text2=Просьба ответить 'Да' или 'Нет' -poll.reply.text3= или -poll.reply.text4={0} {1} +poll.reply.text3=или poll.reply.text5=Ответить poll.reply.text6=Пожалуста, ответьте poll.message.send={0} {1} poll.recipients.validation.error=Выбрать контакты для отправки сообщений -frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name = {1} {0} "{2}" должен быть уникальный +frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name={1} {0} "{2}" должен быть уникальный frontlinesms2.Poll.responses.validator.error.frontlinesms2.Poll.responses=Варианты ответов не могут быть одинаковыми -frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value = Ключевое "{2}" уже используется - +frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value=Ключевое "{2}" уже используется wizard.title.new=Новый wizard.fmessage.edit.title=Редактировать {0} popup.title.saved={0} сохранен! @@ -386,26 +470,25 @@ smallpopup.messages.export.title=Результат экспорта ({0} messag smallpopup.test.message.title=Тестовое сообщение smallpopup.recipients.title=Получатели smallpopup.folder.title=Папка -smallpopup.group.title=Группа smallpopup.contact.export.title=Экспортировать smallpopup.contact.delete.title=Удалить contact.selected.many={0} контакты выбраны group.join.reply.message=Добро пожаловать group.leave.reply.message=Досвидания -fmessage.new.info= У вас {0} новых сообщений. Нажмите чтобы посмотреть -wizard.quickmessage.title=Быстрое Сообщение +fmessage.new.info=У вас {0} новых сообщений. Нажмите чтобы посмотреть +wizard.quickmessage.title=Send Message wizard.messages.replyall.title=Ответить Всем wizard.send.message.title=Отправленые Сообщения wizard.ok=Хорошо wizard.create=Создать +wizard.save=Сохранить wizard.send=Отправить common.settings=Настройки common.help=Помощь - +validation.nospaces.error=Ключевые слова не должны содержать растаяние activity.validation.prompt=Пожалуйста, заполните все обязательные поля +validator.invalid.name=Another activity exists with the name {2} autoreply.blank.keyword=Пустое ключевое слово. Ответ будет отправлен на все входящие сообщения - - poll.type.prompt=Выберите тип опроса poll.question.yes.no=Вопросы с ответом 'Да' или 'Нет' poll.question.multiple=Вопросы с выбором (пример 'Красный', 'Синий', 'Зеленый') @@ -416,13 +499,14 @@ poll.replies.description=Если входящее сообщение опред poll.autoreply.send=Отправить автоматический ответ на отклик опроса poll.responses.prompt=Введите потенциальные ответы (от 2 до 5) poll.sort.header=Сортировка сообщений автоматически по ключевому слову (по желанию) +poll.sort.enter.keywords=Введите ключевые слова для опроса и ответов poll.sort.description=Если люди посылают ответы опроса с помощью ключевых слов, ФронтлайнСМС может автоматически сортировать сообщения в вашей системе. poll.no.automatic.sort=Не сортировать сообщения автоматически poll.sort.automatically=Сортировать сообщения автоматически если есть следующие ключевые слова poll.validation.prompt=Пожалуйста, заполните все обязательные поля poll.name.validator.error.name=Названия опроса должны быть уникальными pollResponse.value.blank.value=Значение ответа Опроса не может быть пустым -poll.alias.validation.error.invalid.alias=Неверный псевдоним. Попробуйте, имя, слово +poll.keywords.validation.error.invalid.keyword=Недесвительный ключивоя слово. Поброй имя, слова poll.question=Введите Вопрос poll.response=Список откликов poll.sort=Атоматическая сортировка @@ -431,6 +515,7 @@ poll.edit.message=Редактировать Сообщение poll.recipients=Выбрать получателей poll.confirm=Подтвердить poll.save=Опрос сохранен! +poll.save.success={0} Опрос сахранён! poll.messages.queue=Если вы хотите отправить сообщение с опросом, сообщения, добавлены в очереди ожидании сообщений. ???????????????????? poll.messages.queue.status=Отправление сообщений может занять некоторое время, в зависимости от количества сообщений и подключения к сети. poll.pending.messages=Чтобы увидеть статус вашего сообщения, окройте папку 'Очередь'. @@ -451,20 +536,19 @@ quickmessage.count.label=Счет Сообщений: quickmessage.messages.label=Введите сообщение quickmessage.phonenumber.label=Добавьте номер телефона: quickmessage.phonenumber.add=Добавить -quickmessage.selected.recipients=получатели выбраны +quickmessage.selected.recipients=Получатели выбраны quickmessage.validation.prompt=Пожалуйста, заполните все обязательные поля - -fmessage.number.error=Символы в этой области, будут удалены при сохранении +fmessage.number.error=Non-numeric characters in this field will be removed when saved search.filter.label=Ограничить поиск до search.filter.group=Выбрать группу search.filter.activities=Выбрать действие/папку search.filter.messages.all=Все исходящие и входящие search.filter.inbox=Только входящие сообщения search.filter.sent=Только исходящие сообщения -search.filter.archive= Добавить/Включить архив +search.filter.archive=Добавить/Включить архив search.betweendates.label=Между датами search.header=Поиск -search.quickmessage=Быстроее сообщение +search.quickmessage=Send message search.export=Экспортировать результат search.keyword.label=Ключевое слово или фраза search.contact.name.label=Контактное лицо @@ -472,32 +556,31 @@ search.contact.name=Контактное лицо search.result.header=Результаты search.moreoptions.label=Доболнительные фукции settings.general=Общий +settings.porting=Import and Export settings.connections=Телефоны и соединения settings.logs=Система settings.general.header=Настройки > Оосновные -settings.logs.header=Журналы Системных Сообщений +settings.logs.header=Settings > System Logs logs.none=У вас нет журнала. logs.content=Сообщение logs.date=Время logs.filter.label=Показать журнал за logs.filter.anytime=все время -logs.filter.1day=последние 24 часа -logs.filter.3days=последние 3 дня -logs.filter.7days=последние 7 дней -logs.filter.14days=последние 14 дней -logs.filter.28days=последние 29 дней +logs.filter.days.1=last 24 hours +logs.filter.days.3=last 3 days +logs.filter.days.7=last 7 days +logs.filter.days.14=last 14 days +logs.filter.days.28=last 28 days logs.download.label=Скачать журналы системы logs.download.buttontext=Скачать журналы logs.download.title=Скачать журналы для отправки logs.download.continue=Продолжать - smartgroup.validation.prompt=Пожалуйста, заполните все необходимые поля. Вы можете выбрать только одно правило на каждое поле. smartgroup.info=Для того чтобы создать Смарт группу, выберите критерий который должен совпадать с контактами в этой группе. -smartgroup.contains.label=содержит +smartgroup.contains.label=Содержит smartgroup.startswith.label=начинается с smartgroup.add.anotherrule=Добавьте другое правило smartgroup.name.label=Название - modem.port=Порт modem.description=Описание modem.locked=Закрытая? @@ -511,30 +594,30 @@ traffic.all.folders.activities=Показать все действия traffic.sent=Отправлено traffic.received=Принято traffic.total=Итого - tab.message=Сообщения tab.archive=Архив tab.contact=Контакты tab.status=Статус tab.search=Поиск - help.info=Это бета версия программы, поэтому встроенная помощь отсутствует. Пожалуйста, зайдите на форумы пользователей, чтобы получить помощь на этом этапе. - +help.notfound=This help file is not yet available, sorry. # IntelliSms Fconnection -intellismsfconnection.label=IntelliSms Счет -intellismsfconnection.type.label=Тип -intellismsfconnection.name.label=Название -intellismsfconnection.username.label=Пользователь -intellismsfconnection.password.label=Пароль - -intellismsfconnection.send.label=Использовать при отправке -intellismsfconnection.receive.label=Использовать при получении -intellismsfconnection.receiveProtocol.label=Протокол -intellismsfconnection.serverName.label=Название Сервера -intellismsfconnection.serverPort.label=Порт Сервера -intellismsfconnection.emailUserName.label=Пользователь -intellismsfconnection.emailPassword.label=Пароль - +intellisms.label=IntelliSms Счет +intellisms.type.label=Тип +intellisms.name.label=Название +intellisms.username.label=Пользователь +intellisms.password.label=Пароль +intellisms.sendEnabled.label=Use for sending +intellisms.receiveEnabled.label=Use for receiving +intellisms.receiveProtocol.label=Протокол +intellisms.serverName.label=Название Сервера +intellisms.serverPort.label=Порт Сервера +intellisms.emailUserName.label=Пользователь +intellisms.emailPassword.label=Пароль +intellisms.description=Отправлять и получать сообщения через счета Intellisms +intellisms.global.info=Вам нужно будет настроить аккаунт с Intellisms (www.intellisms.co.uk). +intelliSmsFconnection.send.validator.invalid=Вы не можете настроить соединение без отправки или получения функциональности +intelliSmsFconnection.receive.validator.invalid=Вы не можете настроить соединение без отправки или получения функциональности #Controllers contact.label=Контакт (ы) contact.edited.by.another.user=Другой пользователь обновил этот Контакт пока вы редактировали @@ -543,15 +626,15 @@ contact.exists.warn=Контакт с этим номером уже сущес contact.view.duplicate=Просмотреть дупликат contact.addtogroup.error=Не можете добавлять и удалять из той же группы! contact.mobile.label=Мобильный -contact.email.label=Электронная почта fconnection.label=Fсвязь fconnection.name=Fсвясь fconnection.unknown.type=Неизвестный тип соединения: fconnection.test.message.sent=Тестовое сообщение отправлено! announcement.saved=Объявление было сохранено и сообщение (я) поставлено (ы) в очередь для отправки announcement.not.saved=Не удалось сохранить объявление! +announcement.save.success={0} Соовщение сохранён! announcement.id.exist.not=Не удалось найти объявления с идентификатором {0} -autoreply.saved=Автоответчик сохранен! +autoreply.save.success={0} Автоответчик был сохранен! autoreply.not.saved=Не удалось сохранить автоответчик! report.creation.error=Ошибка при создании отчета export.message.title=Экспорт Сообщений ФронтлайнСМС @@ -573,36 +656,38 @@ export.messages.name2={0} ({1} сообщения) export.contacts.name1={0} группа ({1} контакты) export.contacts.name2={0} смарт-группа ({1} контакты) export.contacts.name3=Все контакты ({0} контакты) -folder.label=Папка folder.archived.successfully=Папка была успешно архивировонна! folder.unarchived.successfully=Папка успешно извлечена! folder.trashed=Папка в корзине! folder.restored=Папка восстановленна! folder.exist.not=Не удалось найти папку с идентификатором {0} folder.renamed=Папка переименована - group.label=Группа group.name.label=Название group.update.success=Группа успешно обновлена group.save.fail=Не удалось сохранить группу group.delete.fail=Не удалось удалить группу - -import.label=Импортировать -import.backup.label=Импорт данных из предыдущей резервной копии -import.prompt.type=Выберите тип даты для импорта -import.contacts=Детали контакта -import.messages=Детали сообщения -import.version1.info=Чтобы импортировать данные из версии 1, пожалуйста, экспортируйте их на английском языке -import.prompt=Выберите файл данных для импорта +import.label=Import contacts and messages +import.backup.label=You can use the form below to import your contacts. You can also import messages that you have from a previous Frontline project. +import.prompt.type=Select the type of data you wish to import before uploading +import.messages=Messages in Frontline's CSV format +import.prompt=Select the file containing your data to initiate the import import.upload.failed=По не известной причине загрузка файла не удалась. import.contact.save.error=Обнаружена ошибка при сохранении контактов import.contact.complete={0} контактов было импортировано; {1} не удалось -import.contact.failed.download=Скачать не сохраненные контакты (CSV) +import.contact.exist=The imported contacts already exist. +import.contact.failed.label=Failed contact imports +import.contact.failed.info={0} contact(s) successfully imported.
{1} contact(s) could not be imported.
{2} +import.download.failed.contacts=Download a file containing the failed contacts. import.message.save.error=Обнаружена ошибка при сохранении сообщения import.message.complete={0} сообщений было импортировано; {1} не удалось - -many.selected = {0} {1}s выделено - +export.label=Export data from your Frontline workspace +export.backup.label=You can export your Frontline data as VCF/VCard, CSV or PDF +export.prompt.type=Select which data you wish to export +export.allcontacts=All of your contacts +export.inboxmessages=Your Inbox messages +export.submit.label=Export and download data +many.selected={0} {1}s выделено flash.message.activity.found.not=Деятельность не может быть обнаружена flash.message.folder.found.not=Папка не можеть быть обнраружена flash.message=Сообщение @@ -611,7 +696,6 @@ flash.message.fmessages.many={0} СМС сообщения flash.message.fmessages.many.one=1 СМС сообщение fmessage.exist.not=Не удается найти сообщение с идентификатором {0} flash.message.poll.queued=Опрос был сохранен и сообщение (я) были поставлены в очередь для отправки -flash.message.poll.saved=Опрос сохранен flash.message.poll.not.saved=Не удалось сохранить опрос! system.notification.ok=Да system.notification.fail=ОШИБКА @@ -620,36 +704,44 @@ flash.smartgroup.saved=Смарт-группа {0} сохранена flash.smartgroup.save.failed=Не удалось сохранить смарт-группу. Ошибки были {0} smartgroup.id.exist.not=Не удалось найти смарт-группу с идентификатором {0} smartgroup.save.failed=Не удалось сохранить смарт-группы {0} с параметрами {1} {2} ошибки: {3} -contact.name.label=Название -contact.phonenumber.label=Номер телефона - searchdescriptor.searching=Идет поиск -searchdescriptor.all.messages= все сообщения +searchdescriptor.all.messages=все сообщения searchdescriptor.archived.messages=, включая архивированные сообщения searchdescriptor.exclude.archived.messages=, без архивированных сообщений searchdescriptor.only=, только only {0} searchdescriptor.between=, между {0} и {1} searchdescriptor.from=, от {0} searchdescriptor.until=, до {0} -poll.title={0} опрос -announcement.title={0} объявление -autoreply.title={0} автоответчик -folder.title={0} папка +poll.title={0} +announcement.title={0} +autoreply.title={0} +folder.title={0} frontlinesms.welcome=Добро пожаловать в ФронтлайнСМС! \\o/ failed.pending.fmessages={0} Срок ожидания отправки сообщения истёк. Просмотрите папку отложенных сообщений /сообщения в ожидание провалились. Просмотрите раздел ожидающих сообщений. - +subscription.title={0} +subscription.info.group=группа: {0} +subscription.info.groupMemberCount={0} члены +subscription.info.keyword=Верхнего уровня ключевые слова: {0} +subscription.sorting.disable=Отключить автоматическую сортировку +subscription.info.joinKeywords=Присоединиться: {0} +subscription.info.leaveKeywords=Yходить: {0} +subscription.group.goto=Посмотреть группы +subscription.group.required.error=Подписки должны иметь группу +subscription.save.success={0} Подписка был сохранен! language.label=Язык language.prompt=Поменять язык интерфейса пользователя ФронтлайнСМС frontlinesms.user.support=Поддержка пользователей ФронтлайнСМС download.logs.info1=ВНИМАНИЕ!Команда ФронтлайнСМС не может непосредственно реагировать на представленные отчёты. Если у вас есть запрос пожалуйста,проверьте файлы справки поддержки пользователей, чтобы увидеть, можете ли вы найти ответ там. Если нет, сообщить о своей проблеме через наш форум поддержки пользователей: download.logs.info2=Другие пользователи, возможно, тоже сообщили о той же проблеме и возможно уже нашли решение! Чтобы ввести и представить ваши log-файлы, пожалуйста, нажмите "Продолжить" - +# Configuration location info +configuration.location.title=Расположение Конфигурация +configuration.location.description=These files include your database and other settings, which you may wish to back up elsewhere. +configuration.location.instructions=Вы можете найти свои конфигурации приложения на {1} . Эти файлы включают базы данных и другие параметры, которые вы можете создать резервную копию в другом месте. dynamicfield.contact_name.label=Имя контакта dynamicfield.contact_number.label=Номер Контакта dynamicfield.keyword.label=Ключевое слово dynamicfield.message_content.label=Содержание сообщения - -# Fmessage domain +# TextMessage domain fmessage.queued=Сообщение поставлено в очередь для отправки на {0} fmessage.queued.multiple=Сообщение поставлено в очередь для отправки для {0} получателей fmessage.retry.success=Сообщение было назначено для отправки повторно в {0} @@ -659,19 +751,21 @@ fmessage.text.label=Сообщение fmessage.date.label=Дата fmessage.to=Кому: {0} fmessage.to.multiple=Кому: {0} получатели -fmessage.quickmessage=Быстрое сообщение +fmessage.quickmessage=Send message fmessage.archive=Архив fmessage.activity.archive=Архив {0} fmessage.unarchive=Извлечь из архива {0} fmessage.export=Экспортировать fmessage.rename=Переименовать{0} fmessage.edit=Редактировать {0} -fmessage.delete=Удалить {0} +fmessage.delete=Delete fmessage.moreactions=Дополнительные действия... fmessage.footer.show=Показать fmessage.footer.show.failed=Не удалось fmessage.footer.show.all=Все fmessage.footer.show.starred=Поменченные +fmessage.footer.show.incoming=Bходящий +fmessage.footer.show.outgoing=Исходящий fmessage.archive.back=Назад fmessage.activity.sentmessage=({0} сообщений отравлено) fmessage.failed=не удалось @@ -685,37 +779,32 @@ fmessage.resend=Переслать fmessage.retry=Попробовать снова fmessage.reply=Ответить fmessage.forward=Перенаправить -fmessage.unarchive=Извлечь из архива -fmessage.delete=Удалить fmessage.messages.none=Здесь сообщений нет! fmessage.selected.none=Сообщения не выделены fmessage.move.to.header=Переместить сообщения в... fmessage.move.to.inbox=Входящие -fmessage.archive.many=Архивировать все +fmessage.archive.many=Archive selected fmessage.count=1 сообщение fmessage.count.many={0} сообщения -fmessage.many= сообшения -fmessage.delete.many=Удалить Все -fmessage.reply.many=Ответить Всем +fmessage.many=сообшения +fmessage.delete.many=Delete selected +fmessage.reply.many=Reply selected fmessage.restore=Востановить fmessage.restore.many=Востановить -fmessage.retry.many=Повторить не удалось +fmessage.retry.many=Retry selected fmessage.selected.many={0} сообщений выделено -fmessage.unarchive.many=Извлечь все их архива - +fmessage.unarchive.many=Unarchive selected # TODO move to poll.* fmessage.showpolldetails=Показать график fmessage.hidepolldetails=Спрятать график - # TODO move to search.* fmessage.search.none=Сообщений не найдено fmessage.search.description=Начать новый поиск слева - +fmessage.connection.receivedon=Получено: activity.name=Название activity.delete.prompt=Переместить Move {0} в ящик to trash. Это перенест все связанные с ними сообщения в мусорную корзину. activity.label=Деятельность activity.categorize=Классифицировать ответ - magicwand.title=Добавить замены выражения folder.create.success=Успешно удалось создать папку folder.create.failed=Не удалось создать папку @@ -729,7 +818,6 @@ announcement.name.blank.error=Навание объявления не може announcement.name.validator.error=Навание объявления уже используется group.name.blank.error=Название группы не может быть пустым group.name.validator.error=Название группы уже используется - #Jquery Validation messages jquery.validation.required=Это поле является обязательным. jquery.validation.remote=Пожалуйста, исправьте это поле. @@ -748,4 +836,230 @@ jquery.validation.rangelength=Пожалуйста, введите значен jquery.validation.range=Пожалуйста, введите значение от {0} и {1}. jquery.validation.max=Пожалуйста, введите значение меньше или равное {0}. jquery.validation.min=Пожалуйста, введите значение, большее или равное {0}. - +# Webconnection common +webconnection.select.type=Выберите веб-службы или приложения для подключения к: +webconnection.type=Выберите тип +webconnection.title={0} +webconnection.label=TODO:Web Connection +webconnection.description=Подключение к веб-службе. +webconnection.sorting=Автоматическая сортировка +webconnection.configure=Настроить службу +webconnection.api=Pазоблачать API +webconnection.api.info=ФтонрлайнСМС может быть сконфигурирован для приема входящих запросов от удаленного обслуживания и вызвать исходящих сообщений. Более подробную информацию см. в справке раздела Web Connection. +webconnection.api.enable.label=давать возможность API +webconnection.api.secret.label=Секретный ключ: +webconnection.api.disabled=API disabled +webconnection.api.url=TODO:API URL +webconnection.moreactions.retryFailed=retry failed uploads +webconnection.failed.retried=Failed web connections have been scheduled for resending. +webconnection.url.error.locahost.invalid.use.ip=Please use 127.0.0.1 instead of "locahost" for localhost urls +webconnection.url.error.url.start.with.http=Invalid URL (should start with http:// or https://) +# Webconnection - generic +webconnection.generic.label=Другие веб-сервис +webconnection.generic.description=Отправлять сообщения на другие веб-службы +webconnection.generic.subtitle=TODO:HTTP Web Connection +# Webconnection - Ushahidi/Crowdmap +webconnection.ushahidi.label=TODO:Crowdmap / Ushahidi +webconnection.ushahidi.description=Отправлять сообщения CrowdMap или сервер Ushahidi. +webconnection.ushahidi.key.description=Ключ API для любой Crowdmap или Ushahidi можно найти в настройках Crowdmap Ushahidi или веб-сайт. +webconnection.ushahidi.url.label=Ushahidi deployment address: +webconnection.ushahidi.key.label=Ushahidi ключ API: +webconnection.crowdmap.url.label=Crowdmap развертывания адресу: +webconnection.crowdmap.key.label=Crowdmap ключ API: +webconnection.ushahidi.serviceType.label=Выберите службу +webconnection.ushahidi.serviceType.crowdmap=Crowdmap +webconnection.ushahidi.serviceType.ushahidi=Ushahidi +webconnection.crowdmap.url.suffix.label=.crowdmap.com +webconnection.ushahidi.subtitle=Подключение к веб {0} +webconnection.ushahidi.service.label=Cервис: +webconnection.ushahidi.fsmskey.label=ФронтлайнСМС API секрет: +webconnection.ushahidi.crowdmapkey.label=Crowdmap/Ushahidi API Key: +webconnection.ushahidi.keyword.label=Ключевое слово: +url.invalid.url=The URL provided is invalid. +webconnection.confirm=Подтвердить +webconnection.keyword.title=Передача каждого полученного сообщения, содержащие следующие ключевые слова: +webconnection.all.messages=Не используйте ключевые слова (все входящие сообщения будут пересылаться на этот Web Connection +webconnection.httpMethod.label=Выберите HTTP метод: +webconnection.httpMethod.get=Получить +webconnection.httpMethod.post=Поместить +webconnection.name.prompt=Назовите этот веб-соединение +webconnection.details.label=подтвердить детали +webconnection.parameters=Настройка информация отправляется на сервер +webconnection.parameters.confirm=Сконфигурированные данные отправляются на сервер +webconnection.keyword.label=Ключевое слово +webconnection.none.label=Ни один +webconnection.url.label=Cервер Url: +webconnection.param.name=Имя: +webconnection.param.value=значение: +webconnection.add.anotherparam=Добавить параметр +dynamicfield.message_body.label=Текст сообщения +dynamicfield.message_body_with_keyword.label=Текст сообщения с ключевым словом +dynamicfield.message_src_number.label=Контактный номер +dynamicfield.message_src_name.label=Контактное имя +dynamicfield.message_timestamp.label=Cообщение Timestamp +webconnection.keyword.validation.error=Требуется ключевое слово +webconnection.url.validation.error=Tребуется URL +webconnection.save=был сохранен! +webconnection.saved=сохранен! +webconnection.save.success={0} был сохранен! +webconnection.generic.service.label=Обслуживание +webconnection.generic.httpMethod.label=Http метод: +webconnection.generic.url.label=Адрес: +webconnection.generic.parameters.label=Сконфигурированные данные отправляются на сервер: +webconnection.generic.keyword.label=Ключевое слово: +webconnection.generic.key.label=ключ API +frontlinesms2.Keyword.value.validator.error.frontlinesms2.UshahidiWebconnection.keyword.value=Неверное значение ключевого слова +#Subscription i18n +subscription.label=подписка +subscription.name.prompt=Дайте имя етой подписке +subscription.details.label=Подтвердите детали +subscription.description=Позвольте людям автоматически присоединиться и оставить контактные группы с использованием ключевого слова сообщения +subscription.select.group=Выберите группу для подписки +subscription.group.none.selected=Выберите группу +subscription.autoreplies=автоответчики +subscription.sorting=Автоматическая сортировка +subscription.sorting.header=Сообщения процесса автоматически с помощью ключевых слов (не обязательно) +subscription.confirm=подтверждить +subscription.group.header=Выберите группу +subscription.group.description=TODO:Contacts can be added and removed from groups automatically when FrontlineSMS receives a message that includes a special keyword. +subscription.keyword.header=TODO:Enter keywords for this subscription +subscription.top.keyword.description=TODO:Enter the top-level keywords that users will use to select this group. +subscription.top.keyword.more.description=TODO:You may enter multiple top-level keywords for each option, separated with commas. Top-level keywords need to be unique across all activities. +subscription.keywords.header=TODO:Enter keywords for joining and leaving this group. +subscription.keywords.description=TODO:You may enter multiple keywords separated by commas. If no top-level keywords are entered above, then these join and leave keywords need to be unique across all activities. +subscription.default.action.header=TODO:Select an action when no keywords sent +subscription.default.action.description=TODO:Select the desired action when a message matches the top-level keyword but none of the join or leave keywords: +subscription.keywords.leave=TODO:Leave keyword(s) +subscription.keywords.join=TODO:Join keyword(s) +subscription.default.action.join=TODO:Add the contact to the group +subscription.default.action.leave=TODO:Remove the contact from the group +subscription.default.action.toggle=TODO:Toggle the contact's group membership +subscription.autoreply.join=TODO:Send an automatic reply when a contact joins the group +subscription.autoreply.leave=TODO:Send an automatic reply when a contact leaves the group +subscription.confirm.group=группа +subscription.confirm.keyword=ключевое слово +subscription.confirm.join.alias=Регистрация Ключевые слова +subscription.confirm.leave.alias=Оставьте Ключевые слова +subscription.confirm.default.action=Действие по умолчанию +subscription.confirm.join.autoreply=TODO:Join Autoreply +subscription.confirm.leave.autoreply=TODO:Leave Autoreply +subscription.info1=Подписка была сохранена и сейчас активно +subscription.info2=Входящие сообщения, которые соответствуют этим ключевым словом теперь изменить членство в группе контактов, как это определено +subscription.info3=Чтобы увидеть подписки, нажмите на нее в меню слева +subscription.categorise.title=Категоризовать сообщения +subscription.categorise.info=Пожалуйста, выберите действие, которое будет выполняться с отправителями выбранное сообщение, когда они добавляются к {0} +subscription.categorise.join.label=Добавить отправителя в {0} +subscription.categorise.leave.label=Удалить отправителей от {0} +subscription.categorise.toggle.label=TODO:Toggle senders' membership of {0} +subscription.join=присоединиться +subscription.leave=бросать +subscription.sorting.example.toplevel=например РЕШЕНИЕ +subscription.sorting.example.join=например ПОДПИСКА, присоединиться +subscription.sorting.example.leave=например Отказаться от подписки, ОСТАВИТЬ +subscription.keyword.required=Требуется ключевое слово +subscription.jointext.required=TODO:Please enter join autoreply text +subscription.leavetext.required=TODO:Please enter leave autoreply text +subscription.moreactions.delete=Удалить подписку +subscription.moreactions.rename=Переименовать подписки +subscription.moreactions.edit=Редактирование подписки +subscription.moreactions.export=Экспорт подписки +# Generic activity sorting +activity.generic.sorting=Автоматическая обработка +activity.generic.sorting.subtitle=Сообщения процесса автоматически с помощью ключевых слов (не обязательно) +activity.generic.sort.header=Сообщения процесса автоматически с помощью ключевых слов (не обязательно) +activity.generic.sort.description=TODO:If people send in messages beginning with a particular keyword, FrontlineSMS can automatically process the messages on your system. +activity.generic.keywords.title=TODO:Enter keywords for activity. You can enter multiple keywords separated by commas: +activity.generic.keywords.subtitle=Введите ключевые слова для деятельности +activity.generic.keywords.info=TODO:You can enter multiple keywords separated by commas: +activity.generic.no.keywords.title=Не используйте ключевые слова +activity.generic.no.keywords.description=Все входящие сообщения, которые не соответствуют ни другие ключевые слова будут вызывать эту деятельность +activity.generic.disable.sorting=Не автоматической сортировки сообщений +activity.generic.disable.sorting.description=Сообщения не будут автоматически обрабатываются этой деятельности +activity.generic.enable.sorting=TODO:Process responses containing a keyword automatically +activity.generic.sort.validation.unique.error=Ключевые слова должны быть уникальными +activity.generic.keyword.in.use=Ключевое слово {0} уже используется деятельности {1} +activity.generic.global.keyword.in.use=TODO:Activity {0} is set to receive all messages that do not match other keywords. You can only have one active activity with this setting +#basic authentication +auth.basic.label=Basic Authentication +auth.basic.info=Require a username and password for accessing FrontlineSMS across the network +auth.basic.enabled.label=Enable Basic Authentication +auth.basic.username.label=Username +auth.basic.password.label=Password +auth.basic.confirmPassword.label=Confirm Password +auth.basic.password.mismatch=Passwords don't match +newfeatures.popup.title=Новые возможности +newfeatures.popup.showinfuture=Показывать это окно в будущеm +dynamicfield.message_text.label=текст сообщения +dynamicfield.message_text_with_keyword.label=Текст сообщения с ключевым словам +dynamicfield.sender_name.label=Имя Отправителя +dynamicfield.sender_number.label=номер отправителя +dynamicfield.recipient_number.label=номер получателя +dynamicfield.recipient_name.label=Имя получателя +# Smpp Fconnection +smpp.label=SMPP Account +smpp.type.label=Тип +smpp.name.label=Название +smpp.send.label=Use for sending +smpp.receive.label=Use for receiving +smpp.url.label=SMSC URL +smpp.port.label=SMSC Port +smpp.username.label=Username +smpp.password.label=Password +smpp.fromNumber.label=From number +smpp.description=Send and receive messages through an SMSC +smpp.global.info=You will need to get an account with your phone network of choice. +smpp.send.validator.invalid=You cannot configure a connection without SEND or RECEIVE fuctionality. +routing.title=Create rules for which phone number is used by outgoing messages. +routing.info=These rules will determine how the system selects which connection or phone number to use to send outgoing messages. Remember, the phone number seen by recipients may depend on the rules you set here. Also, changing this configuration may affect the cost of sending messages. +routing.rules.sending=When sending outgoing messages: +routing.rules.not_selected=If none of the above rules match: +routing.rules.otherwise=Otherwise: +routing.rules.device=Use {0} +routing.rule.uselastreceiver=Send through most recent number that the contact messaged +routing.rule.useany=Use any available connection's phone number +routing.rule.dontsend=Do not send the message +routing.notification.no-available-route=Outgoing message(s) not sent due to your routing preferences. +routing.rules.none-selected.warning=Warning: You have no rules or phone numbers selected. No messages will be sent. If you wish to send messages, please enable a connection. +customactivity.overview=Overview +customactivity.title={0} +customactivity.confirm=Подтвердить +customactivity.label=Custom Activity Builder +customactivity.description=Create your own activity from scratch by applying a custom set of actions to your specified keyword +customactivity.name.prompt=Name this activity +customactivity.moreactions.delete=Delete activity +customactivity.moreactions.rename=Rename activity +customactivity.moreactions.edit=Edit activity +customactivity.moreactions.export=Export activity +customactivity.text.none=Пусто +customactivity.config=Configure +customactivity.config.description=Build and configure a set of actions for this activity. The actions will all be executed when a message matches the criteria you set on the previous step. +customactivity.info=Your Custom Activity has been created, and any messages containing your keyword will have the specified actions applied to it. +customactivity.info.warning=Without a keyword, all incoming messages will trigger the actions in this Custom Activity. +customactivity.info.note=Note: If you archive the Custom Activity, incoming messages will no longer be sorted for it. +customactivity.save.success={0} activity saved +customactivity.action.steps.label=Action Steps +validation.group.notnull=Please select a group +customactivity.join.description=Joining "{0}" group +customactivity.leave.description=Leaving "{0}" group +customactivity.forward.description=Forwarding with "{0}" +customactivity.webconnectionStep.description=Upload to "{0}" +customactivity.reply.description=Reply with "{0}" +customactivity.step.join.add=Add sender to group +customactivity.step.join.title=Add sender to group* +customactivity.step.leave.add=Удалить отправителей от {0} +customactivity.step.leave.title=Remove sender from group* +customactivity.step.reply.add=Send Autoreply +customactivity.step.reply.title=Enter message to autoreply to sender* +customactivity.step.forward.add=Forward message +customactivity.step.forward.title=Automatically forward a message to one or more contacts +customactivity.manual.sorting=Automatic processing disabled +customactivity.step.webconnectionStep.add=Upload message to a URL +customactivity.step.webconnectionStep.title=Upload message to a URL +customactivity.validation.error.autoreplytext=Reply message is required +customactivity.validation.error.name=Tребуется URL +customactivity.validation.error.url=Tребуется URL +customactivity.validation.error.paramname=Parameter name is required +recipientSelector.keepTyping=Keep typing... +recipientSelector.searching=Идет поиск +validation.recipients.notnull=Please select at least one recipient +localhost.ip.placeholder=your-ip-address diff --git a/plugins/frontlinesms-core/grails-app/i18n/messages_sw.properties b/plugins/frontlinesms-core/grails-app/i18n/messages_sw.properties index 1d0a36b3d..9226a0554 100644 --- a/plugins/frontlinesms-core/grails-app/i18n/messages_sw.properties +++ b/plugins/frontlinesms-core/grails-app/i18n/messages_sw.properties @@ -1,775 +1,873 @@ -# File Info -# FrontlineSMS Swahili translation by the FrontlineSMS team, Nairobi +# FrontlineSMS English translation by the FrontlineSMS team, Nairobi language.name=Kiswahili - # General info -app.version.label=Toleo - +app.version.label=Version # Common action imperatives - to be used for button labels and similar -action.save=Hifadhi - -# Common action imperatives - to be used for button labels and similar -fmessage.selected.many=Risala {0} zimechaguliwa -fmessage.delete.many=Futa zote -autoreply.saved=Jibu la moja kwa moja limehifadhiwa -announcement.confirm=Thibitisha -autoreply.validation.prompt=Tafadhali jaza taarifa zote zinazohitajika -intellismsfconnection.password.label=Nywila -autoreply.message.title=Risala itakayotumwa ya hili jibu la moja kwa moja\: -fmessage.section.trash=Takataka -search.filter.sent=Risala zilizotumwa pekee -fmessage.unarchive.many=Rejesha zote -poll.sort.by=Panga risala kulingana na neno kuu -poll.messages.queue=Risala zimeongezwa kwenye foleni ya risali 'zinazosubiri'. -autoreply.label=Jibu la moja kwa moja -fmessage.move.to.header=Songesha risala kwa... -smartgroup.id.exist.not=Could not find smartgroup with id {0} Kundi-erevu lililo na kitambulisho {0} halikupatikana -poll.sort.keyword=neno kuu -contact.delete=Futa -contact.phonenumber.international.warning=Nambari hii haiko katika international format. Yaweza leta shida kulinganisha na wawasiliani -fmessage.to.label=Kwenda kwa -contact.all.contacts=Waasiliani wote ({0}) -contact.select.all=Chagua wote -folder.trashed=Folda imesongeshwa kwa takataka\! -default.not.inlist.message=Tabia [{0}] ya darasa [{1}] iliyo na kima [{2}] haipatikani kwenye orodha [{3}] -connection.validation.prompt=Tafadhali jaza sehemu zote zinazohitajika -fmessage.to=Kwa\: {0} -archive.folder=Hifadhi folda -contact.export=Hamisha -message.create.prompt=Andika risala -frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value=Neno kuu "{2}" limetumiwa -typeMismatch.java.net.URL=Tabia {0} lazima iwe URL halali -archive.activity.date=Tarehe -typeMismatch.java.net.URI=Tabia {0} lazima iwe URI halali -wizard.fmessage.edit.title=Hariri {0} -default.search.betweendates.title=kati ya tarehe\: -poll.message.none=Usitume risala na uchaguzi huu (kusanya majibu pekee). -fmessage.addsender=Weka kwenye waasiliani -default.updated={0} imesasishwa -flash.message.fmessages.many=Risala {0} za SMS -fmessage.moreactions=Vitendo zaidi... -search.betweendates.label=Kati ya tarehe -logs.date=Wakati -contact.view.duplicate=Tazama inayofanana -messages.trash.confirmation=Hiki kitendo kitafuta takataka na kufuta ujumbe wa kudumu. Je ungetaka kuendelea? -status.devises.header=Vifaa vilivyotambulika -default.search.moresearchoption.label=Utafutaji zaidi\: -search.clear=Futa utafutaji -intellismsfconnection.type.label=Aina -poll.type.prompt=Chagua aina ya uchaguzi unaotaka kuunda -typeMismatch.java.lang.Double=Tabia {0} lazima iwe nambari halali -archive.sent=Hifadhi kisanduku cha risala zilizotumwa -flash.message.poll.saved=Uchaguzi umewekwa -search.export=Pakia matokeo -default.archived.multiple={0} imehifadhiwa -layout.settings.header=Mazingira -group.leave.reply.message=Kwaheri -group.edit=Hariri kikundi -default.home.label=Nyumbani -smallpopup.group.rename.title=Badilisha jina la kikundi -export.contact.email=Barua pepe -poll.question=Andika swali -default.boolean.true=Kweli -poll.reply=Majibu ya moja kwa moja -logs.download.label=Chukua logi -activity.validation.prompt=Tafadhali jaza sehemu zote zinazohitajika -validation.nospaces.error=usiache nafasi kwenye neno -poll.validation.prompt=Jaza sehemu zote zinazohitajika -poll.choice.validation.error.deleting.response=Huwezi futa chagua iliyowekwa -poll.alias.validation.error.invalid.alias=Lakabu hii si sahihi. Jaribu a,jina, neno -default.doesnt.match.message=Tabia [{0}] ya darasa [{1}]iliyo na kima [{2}] hailingani na ruwaza [{3}] inayohitajika -fmessage.failed=Imefeli -poll.response.enabled=Majibu ya moja kwa moja yamewezeshwa -typeMismatch.java.lang.Long=Tabia {0} lazima iwe nambari halali -archive.activity.name=Jina -default.archived={0} imehifadhiwa -flash.message.poll.queued=Uchaguzi umewekwa na risala zikapangwa kwenye foleni ili zitumwe -action.send=Tuma -intellismsfconnection.serverName.label=Jina la server -default.button.apply.label=Badilisha -searchdescriptor.exclude.archived.messages=isiyo na risala zilizohifadhiwa -fmessage.count.many=risala {0} -intellismsfconnection.receive.label=Tumia kupokea risala -fmessage.new.info=Ume pokea risala {0}. Bonyeza kuona -smartgroup.name.label=Jina -connection.edit=Hariri mwunganisho -default.deleted={0} imefutwa -typeMismatch.java.lang.Integer=Tabia {0} lazima iwe nambari halali -announcement.title=Tangazo la {0} -connection.confirm=Thibitisha -export.contact.title=Kupakia kwa waasiliani wa FrontlineSMS -group.delete.fail=Kundi halikuweza kufutwa -default.invalid.max.size.message=Tabia [{0}] ya darasa [{1}] iliyo na kima [{2}] ni juu ya kima kinachohitajika [{3}] -searchdescriptor.until=, hadi {0} -traffic.update.chart=Sasisha chati -search.contact.name.label=Jina la mpokeaji -announcement.moreactions.delete=Futa tangazo -modem.port=Kituo tarishi -default.search.label=Futa utafutaji huu -smallpopup.fmessage.export.title=Pakia -fmessage.section.inbox=Kisanduku pokezi -default.paginate.next=Ijayo -contact.exists.warn=Kuko na mwasiliani anayetumia hiyo nambari. Huwezi kuunda nyingine inayofanana\! -fmessage.section.pending=Risala zitakazotumwa -default.invalid.creditCard.message=Tabia [{0}] ya darasa [{1}] iliyo na kima [{2}] sio nabari halali ya kadi -contact.selected.many=wapokeaji waliyochaguliwa {0} -announcement.moreactions.export=Pakia tangazo -searchdescriptor.searching=Inatufuta -folder.name.label=Jina -default.unarchived.multiple={0} imetohifadhiwa -default.update.failed=Ilifeli kusasisha {0} iliyo na kitambulisho {1} -autoreply.details.label=Thibitisha maelezo -smallpopup.group.delete.title=Futa kikundi -default.invalid.size.message=Tabia [{0}] ya darasa [{1}] iliyo na kima [{2}] haimo katika kiwango kinachohitajika kutoka [{3}] hadi [{4}] -export.selectformat=Chagua fomati -folder.renamed=Jina la folda limebadilishwa -help.info=Hii ni version ya beta kwa hivyo haijajengewa usaidiza. Tafadhali tazama ukumbi wa watumizi kupata usaidizi zaidi -flash.message.fmessages.many.one=Risala 1 ya SMS -connection.deleted=Muunganisho {0} umefutwa -default.updated.message={0} imesasishwa -smartgroup.save.failed=Kundi-erevu {0} kilifeli kuwekwa kilicho na {1}{2} Makosa\: {3} -autoreply.description=Jibu risala zijazo moja kwa moja -poll.recipients=Chagua wapokeaji -poll.yes=Ndio -poll.no=La -announcement.moreactions.rename=Badilisha jina la tangazo -fmessage.footer.show.all=Yote -quickmessage.select.recipients=Chagua Wapokeaji -poll.replies.description=Ukipokea risala inayolingana na jibu la uchaguzi huu, tuma risala kwa mtumaji aliyetuma jibu hilo -autoreply.title=Jibu la Moja Kwa Moja ya {0} -default.unarchive.failed=Kutohifadhi kwa {0} umefeli -announcement.id.exist.not=Tangazo lililo na kitambulisho {0} halikuweza kupatwa -contact.groups.label=Vikundi -default.updated.multiple={0} imesasishwa -search.filter.archive=Tafuta kwenye hifadhi -announcement.not.saved=Tangazo halikuweza kuwekwa -fmessage.queued=Risala imewekwa kwenye foleni ili itumiwe {0} -fmessage.delete=Futa -default.add.label=Ongeza {0} -traffic.sent=Risala zilizotumwa -poll.autosort.no.description=Risala hazitapangwa moja kwa moja -quickmessage.message.count=Nafasi 160 zimebaki (Risala 1) -fmessage.footer.show=Onyesha -connection.route.disconnecting=Ungua... -default.create.label=Unda {0} -smallpopup.fmessage.delete.title=Futa {0} -contact.exists.prompt=Kuko na mwasiliani mwingine anayetumia hiyo nambari -intellismsfconnection.label=Akaunti ya IntelliSms -quickmessage.message.label=Risala -default.null.message=Tabia [{0}] ya darasa [{1}] haiwezi kuwa batilis -smallpopup.group.edit.title=Hariri kikundi -fmessage.to.multiple=Kwa\: watu {0} -poll.message.count=Nafasi 160 zimebaki (Risala 1) -group.moreactions=Vitendo zaidi... -import.contacts=Maelezo ya waasiliani -contact.cancel=Katisha -contact.delete.many=Futa zote -fmessage.export=Hamisha -poll.sort.header=Panga risala kulingana na neno kuu (kwa hiari yako) -fmessage.search.description=Anza kutafuta upya kushoto -report.creation.error=Kulikuwa na kosa katika hali ya kuunda ripoti -poll.message.label=Risala -default.not.unique.message=Tabia [{0}] ya darasa [{1}] iliyo na kima [{2}] ni lazma iwe ghali(unique) -default.not.equal.message=Tabia [{0}] ya darasa [{1}] iliyo na kima [{2}] haiwezi kuwa sawa na [{3}] -commont.help=Msaada -search.filter.group=Chagua kikundi -flash.message.poll.not.saved=Uchaguzi haukuweza kuwekwa -default.invalid.min.size.message=Tabia [{0}] ya darasa [{1}] iliyo na kima [{2}] ni chini ya kima kinachohitajika [{3}] -searchdescriptor.between=, kati ya {0} na {1} -fmessage.retry.success.multiple=Risala {0} zimewekwa kwenye foleni tena ili zitumwe -announcement.details.label=Thibitisha vipengele -settings.general.header=Mazingira > Kwa ujumla -fmessage.rename=Badilisha jina {0} -action.create=Unda -smslibfconnection.pin.label=Utambuzi wa idadi -connection.createtest.number=Nambari +action.ok=OK +action.close=Funga +action.cancel=Cancel +action.done=Done +action.next=Next +action.prev=Rudi Nyuma +action.back=Back +action.create=Create +action.edit=Update +action.rename=Rename +action.save=Save +action.save.all=Save Selected +action.delete=Delete +action.delete.all=Delete Selected +action.send=Send +action.export=Export +# Messages when FrontlineSMS server connection is lost +server.connection.fail.title=Connection to the server has been lost. +server.connection.fail.info=Please restart FrontlineSMS, or close this window. +#Connections: +connection.creation.failed=Connection could not be created. {0} +connection.route.disabled=Deleted connection from {0} to {1}. +connection.route.successNotification=Successfully created connection on {0}. +connection.route.failNotification=Failed to create connection on {1}: {2} [[[edit]](({0}))]. +connection.route.disableNotification=Disconnected connection on {0} +connection.test.sent=Your test message was successfully sent to {0} using {1}. +connection.route.exception={1} +# Connection exception messages +connection.error.org.smslib.alreadyconnectedexception=Device already connected. +connection.error.org.smslib.gsmnetworkregistrationexception=Failed to register with GSM network. +connection.error.org.smslib.invalidpinexception=Incorrect PIN supplied +connection.error.org.smslib.nopinexception=PIN required but not supplied +connection.error.org.smslib.notconnectedexception={0} +connection.error.org.smslib.nosuchportexception=Port not found, or not accessible +connection.error.java.io.ioexception=Port threw an error: {0} +connection.error.frontlinesms2.camel.exception.invalidapiidexception={0} +connection.error.frontlinesms2.camel.exception.authenticationexception={0} +connection.error.frontlinesms2.camel.exception.insufficientcreditexception={0} +connection.error.serial.nosuchportexception=Port cannot be found +connection.error.org.apache.camel.runtimecamelexception=Cannot establish the connection. +connection.header=Settings > Connections +connection.list.none=You have no connections configured. +connection.edit=Edit +connection.delete=Delete +connection.deleted=Connection {0} was deleted. +connection.add=Add new connection +connection.createtest.message.label=Message +connection.route.disable=Disable +connection.send.test.message=Send test message +connection.test.message=Congratulations from FrontlineSMS \\o/ you have successfully configured {0} to send SMS \\o/ +connection.validation.prompt=Please fill in all required fields +connection.select=Select connection types +connection.type=Choose type +connection.details=Enter details +connection.confirm=Confirm +connection.createtest.number=Number +connection.confirm.header=Confirm settings +connection.name.autoconfigured=Auto-configured {0} {1} on port {2}" +status.connection.none=You have no connections configured. +status.devises.header=Detected devices +status.detect.modems=Detect Modems +status.modems.none=No devices have been detected yet. +status.header=Usage Statistics +connectionstatus.not_connected=Not Connected +connectionstatus.connecting=Connecting +connectionstatus.connected=Connected +default.doesnt.match.message=Property [{0}] of class [{1}] with value [{2}] does not match the required pattern [{3}] +default.invalid.url.message=Property [{0}] of class [{1}] with value [{2}] is not a valid URL +default.invalid.creditCard.message=Property [{0}] of class [{1}] with value [{2}] is not a valid credit card number +default.invalid.email.message=Property [{0}] of class [{1}] with value [{2}] is not a valid e-mail address +default.invalid.range.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid range from [{3}] to [{4}] +default.invalid.size.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid size range from [{3}] to [{4}] +default.invalid.max.message=Property [{0}] of class [{1}] with value [{2}] exceeds maximum value [{3}] +default.invalid.min.message=Property [{0}] of class [{1}] with value [{2}] is less than minimum value [{3}] +default.invalid.max.size.message=Property [{0}] of class [{1}] with value [{2}] exceeds the maximum size of [{3}] +default.invalid.min.size.message=Property [{0}] of class [{1}] with value [{2}] is less than the minimum size of [{3}] +default.invalid.validator.message=Property [{0}] of class [{1}] with value [{2}] does not pass custom validation +default.not.inlist.message=Property [{0}] of class [{1}] with value [{2}] is not contained within the list [{3}] +default.blank.message=Property [{0}] of class [{1}] cannot be blank +default.not.equal.message=Property [{0}] of class [{1}] with value [{2}] cannot equal [{3}] +default.null.message=Property [{0}] of class [{1}] cannot be null +default.not.unique.message=Property [{0}] of class [{1}] with value [{2}] must be unique +default.paginate.prev=Previous +default.paginate.next=Next +default.boolean.true=True +default.boolean.false=False +default.date.format=ss MMMM, mmmm ss:dd +default.number.format=0 +default.unarchived={0} unarchived +default.unarchive.failed=Unarchiving {0} failed +default.trashed={0} moved to trash +default.restored={0} restored +default.restore.failed=Could not restore {0} with id {1} +default.archived={0} archived +default.archived.multiple={0} archived +default.created={0} created +default.created.message={0} {1} has been created +default.create.failed=Failed to create {0} +default.updated={0} has been updated +default.update.failed=Failed to update {0} with id {1} +default.updated.multiple={0} have been updated +default.updated.message={0} updated +default.deleted={0} deleted +default.trashed.multiple={0} moved to trash +default.unarchive.keyword.failed=Unarchiving {0} failed. Keyword or name in use +default.unarchived.multiple={0} unarchived +default.delete.failed=Could not delete {0} with id {1} +default.notfound=Could not find {0} with id {1} +default.optimistic.locking.failure=Another user has updated this {0} while you were editing +default.home.label=Home +default.list.label={0} List +default.add.label=Add {0} +default.new.label=New {0} +default.create.label=Create {0} +default.show.label=Show {0} +default.edit.label=Edit {0} +search.clear=Clear search +default.button.create.label=Create +default.button.edit.label=Edit +default.button.update.label=Update +default.button.delete.label=Delete +default.button.search.label=Search +default.button.apply.label=Apply +default.button.delete.confirm.message=Are you sure? +default.deleted.message={0} deleted +# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) +typeMismatch.java.net.URL=Property {0} must be a valid URL. +typeMismatch.java.net.URI=Property {0} must be a valid URI. +typeMismatch.java.util.Date=Property {0} must be a valid date. +typeMismatch.java.lang.Double=Property {0} must be a valid number. +typeMismatch.java.lang.Integer=Property {0} must be a valid number. +typeMismatch.java.lang.Long=Property {0} must be a valid number. +typeMismatch.java.lang.Short=Property {0} must be a valid number. +typeMismatch.java.math.BigDecimal=Property {0} must be a valid number. +typeMismatch.java.math.BigInteger=Property {0} must be a valid number. +typeMismatch.int={0} must be a valid number. +# Application specific messages +messages.trash.confirmation=This action will delete your messages permanently. Do you want to continue? +default.created.poll=Your poll has been created! +default.search.label=Clear search +default.search.betweendates.title=Between dates: +default.search.moresearchoption.label=More search options +default.search.date.format=s/M/mmmm +default.search.moreoption.label=More options +clickatellFconnection.fromNumber.validator.invalid=A 'From Number' is required for sending messages to the United States. +# Messages Tab +message.create.prompt=Enter message +message.character.count=Characters remaining: {0} ({1} SMS message(s)) +message.character.count.warning=May be longer after performing substitutions. +message.header.inbox=Inbox +message.header.sent=Sent +message.header.pending=Pending +message.header.trash=Trash +message.header.folder=Folders +message.header.activityList=ActivityList +message.header.folderList=FolderList +announcement.label=Announcement +announcement.description=Send an announcement to your contacts and organize the responses +announcement.info1=The announcement has been saved. The messages have been added to the pending message queue. +announcement.info2=It may take some time for all of your messages to be sent, depending on the number of messages and the network connection. +announcement.info3=To see the status of your messages, go to 'Pending' messages. +announcement.info4=To see the announcement, click on it in the left-hand menu. +announcement.validation.prompt=Please fill in all required fields +announcement.select.recipients=Select recipients +announcement.confirm=Confirm +announcement.delete.warn=Delete {0} WARNING: This cannot be undone! +announcement.prompt=Name this announcement +announcement.confirm.message=Message +announcement.details.label=Confirm details +announcement.message.label=Message +announcement.message.none=none +announcement.recipients.label=Recipients +announcement.create.message=Create message +#TODO embed javascript values +announcement.recipients.count=contacts selected +announcement.messages.count=messages will be sent +announcement.moreactions.delete=Delete announcement +announcement.moreactions.rename=Rename announcement +announcement.moreactions.edit=Edit announcement +announcement.moreactions.export=Export announcement +frontlinesms2.Announcement.name.unique.error.frontlinesms2.Announcement.name={1} {0} "{2}" must be unique +archive.inbox=Inbox archive +archive.sent=Sent archive +archive.activity=Activity archive +archive.folder=Folder archive +archive.folder.name=Name +archive.folder.date=Date +archive.folder.messages=Messages +archive.folder.none=  No archived folders +archive.activity.name=Name +archive.activity.type=Type +archive.activity.date=Date +archive.activity.messages=Messages +archive.activity.list.none=  No archived activities +archive.header=Archive +autoreply.enter.keyword=Enter keyword +autoreply.create.message=Enter an auto reply message +activity.autoreply.sort.description=Anyone sending you a message beginning with your chosen keyword will receive your Autoreply. Messages not beginning with the keyword will go to your inbox. +activity.autoreply.disable.sorting.description=Messages from your contacts will go into your inbox and will not receive an automatic response. You will need to manually move messages into this activity in order for contacts to receive the Autoreply. +autoreply.confirm=Confirm +autoreply.name.label=Message +autoreply.details.label=Confirm details +autoreply.label=Autoreply +autoreply.keyword.label=Keyword(s) +autoreply.description=Automatically respond to incoming messages. +autoreply.info=Your auto reply activity has been created, and any messages containing your keyword will be added to this auto reply, which can be viewed by clicking on it in the right-hand menu. +autoreply.info.warning=Without a keyword, your auto reply will be sent to all incoming messages. +autoreply.info.note=Note: If you archive the auto reply, incoming messages will no longer be sorted into this activity. +autoreply.validation.prompt=Please fill in all required fields +autoreply.message.title=Message to be sent back for this auto reply: +autoreply.keyword.title=Sort messages automatically using a keyword: +autoreply.name.prompt=Name this autoreply activity +autoreply.message.count=0 characters (1 SMS message) +autoreply.moreactions.delete=Delete auto reply activity +autoreply.moreactions.rename=Rename auto reply activity +autoreply.moreactions.edit=Edit auto reply activity +autoreply.moreactions.export=Export auto reply activity +autoreply.all.messages=Do not use keyword (All incoming messages will receive this autoreply) +autoreply.text.none=None +frontlinesms2.Autoreply.name.unique.error.frontlinesms2.Autoreply.name={1} {0} "{2}" must be unique +frontlinesms2.Autoreply.name.validator.error.frontlinesms2.Autoreply.name=Autoreply name must be unique +frontlinesms2.Keyword.value.validator.error.frontlinesms2.Autoreply.keyword.value=Keyword "{2}" is already in use +frontlinesms2.Autoreply.autoreplyText.nullable.error.frontlinesms2.Autoreply.autoreplyText=Message cannot be blank +autoforward.title={0} +autoforward.label=Sambaza kioto +autoforward.description=Automatically forward incoming messages to contacts +autoforward.recipientcount.current=Currently {0} recipients +autoforward.create.message=Enter message +autoforward.confirm=Confirm +autoforward.recipients=Recipients +autoforward.name.prompt=Name this Autoforward +autoforward.details.label=Confirm details +autoforward.keyword.label=Keyword(s) +autoforward.name.label=Message +autoforward.contacts=Contacts +autoforward.groups=Groups +autoforward.info=Your auto forward activity has been created, and any messages containing your keyword will be added to this activity, which can be viewed by clicking on it in the right-hand menu. +autoforward.info.warning=Without a keyword, all incoming messages being forwarded. +autoforward.info.note=Note: If you archive this, incoming messages will no longer be sorted into this activity. +autoforward.save=The auto forward activity has been saved! +autoforward.save.success={0} Autoforward has been saved! +autoforward.global.keyword=None (all incoming messages will be processed) +autoforward.disabled.keyword=None (automatic sorting disabled) +autoforward.keyword.none.generic=None +autoforward.groups.none=None +autoforward.contacts.none=None +autoforward.message.format=Message +contact.new=New Contact +contact.list.no.contact=No contacts have been added. +contact.header=Contacts +contact.header.group=Contacts >> {0} +contact.all.contacts=All contacts ({0}) +contact.create=Create new contact +contact.groups.header=Groups +contact.create.group=Create new group +contact.smartgroup.header=Smart groups +contact.create.smartgroup=Create new smart group +contact.add.to.group=Add to group... +contact.remove.from.group=Remove from group +contact.customfield.addmoreinformation=Add more information... +contact.customfield.option.createnew=Create new... +contact.name.label=Name +contact.phonenumber.label=Mobile +contact.notes.label=Notes +contact.email.label=Email +contact.groups.label=Groups +contact.notinanygroup.label=Not part of any Groups +contact.messages.label=Messages +contact.messages.sent={0} messages sent +contact.received.messages={0} messages received +contact.search.messages=Search for messages +contact.select.all=Select All +contact.not.found=Contact not found +group.not.found=Group not found +smartgroup.not.found=Smart Group not found +group.rename=Rename group +group.edit=Edit group +group.delete=Delete group +group.moreactions=More actions... +customfield.validation.prompt=Please fill in a name +customfield.validation.error=Name already exists +customfield.name.label=Name +export.contact.info=To export contacts from your Frontline account, choose the type of export and the information to be included in the exported data. +export.message.info=To export messages from your Frontline account, choose the type of export and the information to be included in the exported data. +export.selectformat=Select an output format +export.csv=CSV format for use in spreadsheet +export.pdf=PDF format for printing +folder.name.label=Name +group.delete.prompt=Are you sure you want to delete {0}? WARNING: This cannot be undone. +layout.settings.header=Settings +activities.header=Activities +activities.create=Create new activity +folder.header=Folders +folder.create=Create new folder +folder.label=Folder +message.folder.header={0} Folder +fmessage.trash.actions=Trash actions... +fmessage.trash.empty=Empty trash +fmessage.to.label=To +trash.empty.prompt=All messages and activities in the trash will be deleted permanently. +fmessage.responses.total={0} responses total +fmessage.label=Message +fmessage.label.multiple={0} messages +poll.prompt=Name this poll +poll.details.label=Confirm details +poll.message.label=Message +poll.choice.validation.error.deleting.response=A saved choice cannot have an empty value. +poll.alias=Aliases +poll.keywords=Keywords +poll.aliases.prompt=Enter any aliases for the corresponding options. +poll.keywords.prompt.details=The top-level keyword will name the poll and be sent in the poll instructions message. Each response can also have alternative short cut keywords. +poll.keywords.prompt.more.details=You may enter multiple keywords separated by commas for the top-level and responses. If no top-level keywords are entered below, then these response keywords need to be unique across all activities. +poll.keywords.response.label=Response Keywords +poll.response.keyword=Set response keywords +poll.set.keyword=Set a top-level keyword +poll.keywords.validation.error=Keywords should be unique +poll.sort.label=Auto-sort +poll.autosort.no.description=Don't sort responses automatically. +poll.autosort.description=Sort responses automatically. +poll.sort.keyword=keyword +poll.sort.toplevel.keyword.label=Top-level keyword(s) (optional) +poll.sort.by=Sort by +poll.autoreply.label=Auto-reply +poll.autoreply.none=none +poll.recipients.label=Recipients +poll.recipients.none=None +poll.toplevelkeyword=Top-level keywords +poll.sort.example.toplevel=e.g TEAM +poll.sort.example.keywords.A=e.g A, AMAZING +poll.sort.example.keywords.B=e.g B, BEAUTIFUL +poll.sort.example.keywords.C=e.g C, COURAGEOUS +poll.sort.example.keywords.D=e.g D, DELIGHTFUL +poll.sort.example.keywords.E=e.g E, EXEMPLARY +poll.sort.example.keywords.yn.A=e.g YES, YAP +poll.sort.example.keywords.yn.B=e.g No, NOP +#TODO embed javascript values +poll.recipients.count=contacts selected +poll.messages.count=messages will be sent +poll.yes=Yes +poll.no=No +poll.label=Poll +poll.description=Send a question and analyze the responses +poll.messages.sent={0} messages sent +poll.response.enabled=Auto Response Enabled +poll.message.edit=Edit message to be sent to recipients +poll.message.prompt=The following message will be sent to the recipients of the poll +poll.message.count=Characters remaining: 160 (1 SMS message) +poll.moreactions.delete=Delete poll +poll.moreactions.rename=Rename poll +poll.moreactions.edit=Edit poll +poll.moreactions.export=Export poll +folder.moreactions.delete=Delete folder +folder.moreactions.rename=Rename folder +folder.moreactions.export=Export folder +#TODO embed javascript values +poll.reply.text=Reply "{0}" for Yes, "{1}" for No. +poll.reply.text1={0} "{1}" for {2} +poll.reply.text2=Please answer 'Yes' or 'No' +poll.reply.text3=or +poll.reply.text5=Reply +poll.reply.text6=Please answer poll.message.send={0} {1} -connection.route.connecting=Unganisha... -activity.categorize=Weka majibu kwa kategoria zinazofaa -default.invalid.range.message=Tabia [{0}] ya darasa [{1}] iliyo na kima [{2}] haimo katika kiwango kinachohitajika kutoka [{3}] hadi [{4}] -activity.label=Kitendo -search.keyword.label=Neno kuu -emailfconnection.receiveProtocol.label=Itifaki -default.create.failed=Kuunda kwa {0} kumefeli -intellismsfconnection.serverPort.label=Kituo cha server -smallpopup.fmessage.rename.title=Badilisha jina la {0} -popup.help.title=Msaada -poll.replies.header=Tuma jibu moja kwa moja wa uchaguzi huu(kwa hiari yako) -fmessage.queued.multiple=Risala imewekwa kwenye ili itumiwe {0} -fmessage.trash.empty=Futa takataka -poll.autoreply.none=Hakuna -logs.download.title=Chukua logi ilizitumwe -traffic.filter.reset=Rudisha mwanzo -group.join.reply.message=Karibu -activity.delete.prompt=Hamisha {0} ziende kwa takataka . Kitendo hiki kitahamisha risala zote ziende kwa takataka -wizard.create=Unda -searchdescriptor.archived.messages=, ikiwa na risala zilizohifadhiwa -fmessage.footer.show.failed=Ilifeli -autoreply.enter.keyword=Neno kuu -language.label=Lugha -fconnection.name=Fconnection -autoreply.info.warning=Moja kwa moja bila neno kuu itatumwa kwa risala zote zijazo -smallpopup.group.title=Kikundi -export.database.id=Kitambulisho cha hifadhidata -connection.route.successNotification=Njia imeundwa katika {0} na mafanikio -action.close=Close -autoreply.create.message=Weka risala -contact.new=Mwasiliani mpya +poll.recipients.validation.error=Select contacts to send messages to +frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name={1} {0} "{2}" must be unique +frontlinesms2.Poll.responses.validator.error.frontlinesms2.Poll.responses=Response options can not be identical +frontlinesms2.Keyword.value.validator.error.frontlinesms2.Poll.keyword.value=Keyword "{2}" is already in use +wizard.title.new=New +wizard.fmessage.edit.title=Edit {0} +popup.title.saved={0} saved! +popup.activity.create=Create New Activity : Select type +popup.smartgroup.create=Create smart group +popup.help.title=Help +smallpopup.customfield.create.title=Create Custom Field +smallpopup.group.rename.title=Rename group +smallpopup.group.edit.title=Edit group +smallpopup.group.delete.title=Delete group +smallpopup.fmessage.rename.title=Rename {0} +smallpopup.fmessage.delete.title=Delete {0} +smallpopup.fmessage.export.title=Export +smallpopup.delete.prompt=Delete {0} ? +smallpopup.delete.many.prompt=Delete {0} contacts? +smallpopup.empty.trash.prompt=Empty Trash? +smallpopup.messages.export.title=Export Results ({0} messages) +smallpopup.test.message.title=Test message +smallpopup.recipients.title=Recipients +smallpopup.folder.title=Folder +smallpopup.group.create.title=Group +smallpopup.contact.export.title=Export +smallpopup.contact.delete.title=Delete +contact.selected.many={0} contacts selected +group.join.reply.message=Welcome +group.leave.reply.message=Bye +fmessage.new.info=You have {0} new messages. Click to view +wizard.quickmessage.title=Send Message +wizard.messages.replyall.title=Reply All +wizard.send.message.title=Send Message +wizard.ok=Ok +fmessage.move.to.inbox=Inbox +traffic.filter.between.dates=Between dates +wizard.create=Create +wizard.send=Send +common.settings=Settings +common.help=Help +validation.nospaces.error=Keyword should not have spaces +activity.validation.prompt=Please fill in all required fields +autoreply.blank.keyword=Note: blank keyword. A response will be sent to all incoming messages. +poll.type.prompt=Select the kind of poll to create +poll.question.yes.no=Question with a 'Yes' or 'No' answer +poll.question.multiple=Multiple choice question (e.g. 'Red', 'Blue', 'Green') +poll.question.prompt=Enter question +poll.message.none=Do not send a message for this poll (collect responses only). +poll.replies.header=Reply automatically to poll responses (optional) +poll.replies.description=When an incoming message is identified as a poll response, send a message to the person who sent the response. +poll.autoreply.send=Send an automatic reply to poll responses +poll.responses.prompt=Enter possible responses (between 2 and 5) +poll.sort.header=Sort messages automatically using a keyword (optional) +poll.sort.enter.keywords=Enter keywords for the poll and the responses +poll.sort.description=If users send poll responses with a specific keyword, then Frontline products can automatically sort the messages into this activity. +poll.no.automatic.sort=Do not sort messages automatically +poll.sort.automatically=Sort messages automatically that have the following keyword: +poll.validation.prompt=Please fill in all required fields +poll.name.validator.error.name=Poll names must be unique +pollResponse.value.blank.value=Poll response value can't be blank +poll.keywords.validation.error.invalid.keyword=Invalid keyword. Try a, name, word +poll.question=Enter Question +poll.response=Response list +poll.sort=Automatic sorting +poll.reply=Automatic reply +poll.edit.message=Edit Message +poll.recipients=Select recipients +poll.confirm=Confirm +poll.save=The poll has been saved! +poll.save.success={0} Poll has been saved! +poll.messages.queue=If you chose to send a message with this poll, the messages have been added to the pending message queue. +poll.messages.queue.status=It may take some time for all the messages to be sent, depending on the number of messages and the network connection. +poll.pending.messages=To see the status of your message, open the 'Pending' messages folder. +poll.send.messages.none=No messages will be sent +quickmessage.details.label=Confirm details +autoreply.not.saved=Your auto reply activity could not be saved. +export.message.date.created=Date Created +tab.status=Status +quickmessage.enter.message=Enter message +quickmessage.message.label=Message +quickmessage.message.none=None +quickmessage.recipient.label=Recipient +quickmessage.recipients.label=Recipients +quickmessage.message.count=Characters remaining: 160 (1 SMS message) +quickmessage.select.recipients=Select recipients +quickmessage.confirm=Confirm +#TODO embed javascript values +quickmessage.recipients.count=contacts selected +quickmessage.messages.count=messages will be sent +quickmessage.count.label=Message Count: +quickmessage.messages.label=Enter message +quickmessage.phonenumber.label=Add phone number: +quickmessage.phonenumber.add=Add +quickmessage.selected.recipients=recipients selected +quickmessage.validation.prompt=Please fill in all required fields +fmessage.number.error=Non-numeric characters in this field will be removed when saved +search.filter.label=Limit Search to +search.filter.group=Select group +search.filter.activities=Select activity/folder +search.filter.messages.all=All sent and received +search.filter.inbox=Only received messages +search.filter.sent=Only sent messages +search.filter.archive=Include Archive +search.betweendates.label=Between dates +search.header=Search +search.quickmessage=Send message +search.export=Export results +search.keyword.label=Keyword +search.contact.name.label=Contact name +search.contact.name=Contact name +search.result.header=Results +search.moreoptions.label=More options +settings.general=General +settings.connections=Phones and connections +settings.logs=System Logs +settings.general.header=Settings > General +settings.logs.header=Settings > System Logs +logs.none=You have no logs. +logs.content=Message +logs.date=Time +logs.filter.label=Show logs for +logs.filter.anytime=all time +logs.download.label=Download system logs +logs.download.buttontext=Download Logs +logs.download.title=Download logs to send +logs.download.continue=Continue +smartgroup.validation.prompt=Please fill in all the required fields. You may only specify one rule per field. +smartgroup.info=To create a Smart Group, select the criteria you need to be matched for contacts for this group. +smartgroup.contains.label=contains +smartgroup.startswith.label=starts with +smartgroup.add.anotherrule=Add another rule +smartgroup.name.label=Name +modem.port=Port +modem.description=Description +modem.locked=Locked? +traffic.header=Traffic +traffic.update.chart=Update chart +traffic.filter.2weeks=Show last two weeks +traffic.filter.reset=Reset Filters +traffic.allgroups=Show all groups +traffic.all.folders.activities=Show all activities/folders +traffic.sent=Sent +traffic.received=Received +traffic.total=Total +tab.message=Messages +tab.archive=Archive +tab.contact=Contacts +tab.search=Search +help.info=This version is a beta release, so there are no built-in help files. Please go to the user forums online to get help at this stage +intelliSmsFconnection.send.validator.invalid=You cannot configure a connection without SEND or RECEIVE functionality. +intelliSmsFconnection.receive.validator.invalid=You cannot configure a connection without SEND or RECEIVE functionality. +#Controllers +contact.label=Contact(s) +contact.edited.by.another.user=Another user has updated this Contact while you were editing. +contact.exists.prompt=There is already a contact with that number. +contact.exists.warn=A contact with this number already exists. +contact.view.duplicate=View duplicate +contact.addtogroup.error=You cannot add and remove from the same group. +contact.mobile.label=Mobile fconnection.label=Fconnection -fmessage.responses.total=Majibu kwa ujumla {0} -clickatellfconnection.type.label=Aina -logs.filter.label=Onyesha logi za -smallpopup.customfield.create.title=Unda Custom Field -flash.smartgroup.saved=Kikundi-erevu {0} kimewekwa -fmessage.archive.back=Rudi nyuma -smslibfconnection.imsi.label=Utambulisho wa Kimataifa wa simu ya mteja -contact.search.messages=Tafuta risala -logs.none=Huna logi. -export.messages.name2={0} ({1} risala) -export.messages.name1={0} {1} ({2} risala) -poll.sort.label=Panga moja kwa moja -folder.archived.successfully=Folda imehifadhiwa vizuri\! -action.delete=Futa -default.restore.failed=Haikuweza kurudisha {0} iliyo na kitambulisho {1} -fmessage.exist.not=Risala iliyo na kitambulisho {0} haijapatikana -archive.activity.list.none=  Hakuna matendo yaliyohiifadhiwa -dynamicfield.message_content.label=Mwili wa risala -flash.message.folder.found.not=Folda haikuweza kupatikana -system.notification.fail=Imefeli -export.contact.name=Jina -archive.folder.messages=Risala -action.prev=Previous -autoreply.info=Moja kwa moja imeundwa. Risala yoyote ambayo iko na neno kuu itaongezwa katika moja kwa moja hii, ambayo inaweza kutazamwa ukiibonyeza kwa menyu iliyo upande wa kulia. -fmessage.retry=Rudia -action.export=Pakia -folder.restored=Folda imerejeshwa\! -logs.filter.7days=Siku 7 zilizopita -intellismsfconnection.receiveProtocol.label=Protokoli -quickmessage.recipients.count=Wapokeaji wamechaguliwa -connectionstatus.connected=Imeunganishwa -wizard.messages.replyall.title=Jibu zote -fmessage.count=risala 1 -quickmessage.recipients.label=Wapokeaji -smslibfconnection.type.label=Aina -import.messages=Maelezo ya risala -modem.locked=Imefungwa? -traffic.total=Kwa ujumla -fmessage.label=Risala -fmessage.section.sent=Risala zilizotumwa -announcement.select.recipients=Chagua wapokeaji -smallpopup.messages.export.title=Pakia Matokeo ({0} risala) -export.contact.info=Kupakia waasiliani kutoka FrontlineSMS, chagua aina ya upakiaji na ujumbe utakao ongezwa katika data inayopakiwa. -archive.activity=Hifadhi matendo -action.rename=Badilisha Jina -contact.addtogroup.error=Huwezi kuongeza na kutoa kutoka kikundi kimoja -autoreply.keyword.label=Neno kuu -announcement.description=Tuma risala za tangazo na uyapange majibu -contact.customfield.addmoreinformation=Ongeza habari zaidi... -frontlinesms2.Announcement.name.unique.error.frontlinesms2.Announcement.name=Jina "{2}" lazima liwe lakipekee -wizard.quickmessage.title=Risala ya haraka -activities.header=Vitendo -contact.create.group=Unda kikundi kipya -status.connection.none=Hauna miunganisho iliyosanidiwa -connection.details=Andika maelezo -intellismsfconnection.name.label=Jina -smallpopup.empty.trash.prompt=Mwaga Tupio? -frontlinesms.user.support=usaidizi wa watumiaji kutoka FrontlineSMS -export.message.text=Andiko -emailfconnection.username.label=jina la mtumiaji -fmessage.showpolldetails=Onyesha grafu -fmessage.activity.sentmessage=(risala {0} zimetumwa) -search.moreoptions.label=Chaguo zaidi -modem.description=Maelezo -folder.header=Folda -tab.message=Risala -search.filter.label=Zuwuia utafutaji uwe katika\: -smslibfconnection.serial.label=Kufuatanisha cha kifaa\# -connection.route.destroyNotification=Njia imekatika katika {0} -default.optimistic.locking.failure=Mtumiaji mwingine amesasisha {0} ulipokuwa ukihariri -logs.filter.28days=Siku 28 zilizopita -import.prompt.type=Chagua aina ya data utakayoingiliza -default.invalid.max.message=Tabia [{0}] ya darasa [{1}] iliyo na kima [{2}] ni juu ya kima kinachohitajika [{3}] -default.invalid.url.message=Tabia [{0}] ya darasa [{1}] iliyo na kima [{2}] sio URL halali -folder.exist.not=Folda iliyo na kitambulisho {0} haikuweza kupatikana -default.trashed={0} imewekwa katika takataka -default.trashed.multiple={0} imewekwa katika takataka -connection.list.none=Huna miunganisho iliyo sanidiwa -popup.smartgroup.create=Unda kikundi-erevu -clickatellfconnection.password.label=nywila -default.restored={0} imerudishwa -contact.add.to.group=Ongezea kikundi... -default.invalid.validator.message=Tabia [{0}] ya darasa [{1}] iliyo na kima [{2}] haipitishi mahitaji yanayohitajika kuhalalishwa -fmessage.edit=Hariri {0} -connection.creation.failed=Kuunda kwa muunganisho huu umefeli -connection.add=Ongeza mwunganisho mpya -import.message.save.error=Kulikuwa na kosa katika kuwekwa kwa risala -poll.autosort.description=Panga risala kulingana na neno kuu -import.label=Ingiliza -logs.download.continue=Endelea -language.prompt=Badilisha lugha unayotumia kwa FrontlineSMS -fmessage.activity.archive=Hifadhi {0} -connection.route.destroy=Ungua mwunganisho -intellismsfconnection.emailUserName.label=Jina la mtumiaji -poll.label=Uchaguzi -dynamicfield.keyword.label=Neno kuu -status.detect.modems=Tambua modemu -typeMismatch.java.util.Date=Tabia {0} lazima iwe tarehe halali -contact.received.messages=Risala zilizopokewa -contact.remove.from.group=Toa kutoka kikundi -quickmessage.phonenumber.add=Ongeza -smallpopup.delete.many.prompt=Futa {0} contacts? -poll.autoreply.send=Tuma risala ya moja kwa moja kwa watumaji wa majibu ya uchaguzi huu -frontlinesms2.Autoreply.name.unique.error.frontlinesms2.Autoreply.name=Jina "{2}" lazima liwe lakipekee -fmessage.retry.success=Risala imewekwa kwenye foleni tena ili itumiwe {0} -default.created.poll=Uchaguzi umeundwa\! -dynamicfield.contact_name.label=Jina la mwasiliani -typeMismatch.java.math.BigInteger=Tabia {0} lazima iwe nambari halali -announcement.create.message=Andika risala -fmessage.messages.none=Hakuna risala hapa\! -frontlinesms2.Keyword.value.validator.error.frontlinesms2.Autoreply.keyword.value=Neno kuu "{2}" limetumiwa -typeMismatch.java.math.BigDecimal=Tabia {0} lazima iwe nambari halali -fmessage.forward=Tuma -fmessage.restore.many=Rudisha -tab.contact=Waasiliani -contact.mobile.label=Nambari ya simu -flash.message=Risala -action.edit=Hariri -wizard.title.new=Mpya -trash.empty.prompt=Risala na vitendo vyote kwenye takataka zitafutwa kabisa -fmessage.restore=Rudisha -status.modems.none=Hakuna vifaa vinavyotambulika -flash.smartgroup.save.failed=Kuweka kwa kundi-erevu kumefeli. Kosa likuwa {0} -default.created.message={0} {1} imeundwa -contact.messages.label=Risala -folder.create.success=Folda kimeundwa vyema -folder.create.failed=Kuunda kwa folda kumefeli -folder.name.validator.error=Jina hili la folda limetumika -folder.name.blank.error=Tafadhali weka jina la folda -folder.moreactions.delete=Futa folda -folder.moreactions.rename=Badilisha jina la folda -folder.moreactions.export=Pakia folda -poll.name.blank.error=Tafadhali weka jina la uchaguzi -poll.name.validator.error=Jina hili la uchaguzi limetumika -autoreply.name.blank.error=Tafadhali weka jina la moja kwa moja -autoreply.name.validator.error=Jina hili la moja kwa moja limetumika -announcement.name.blank.error=Tafadhali weka jina la tangazo -announcement.name.validator.error=Jina hili la tangazo limetumika -group.name.blank.error=Tafadhali weka jina la kikundi -group.name.validator.error=Jina hiki cha kikundi kimetumika - -autoreply.moreactions.delete=Futa jibu la moja kwa moja -poll.reply.text6=Tafadhali jibu -poll.reply.text5=Jibu -poll.reply.text4={0} {1} -poll.reply.text3=ama -poll.reply.text2=Tafadhali jibu 'Ndio' au 'La' -poll.recipients.validation.error=Chagua waasiliani wa kutumia risala -smallpopup.contact.export.title=Pakia -poll.reply.text1={0} "{1} {2}" kujibu {3} -traffic.header=Trafiki -emailfconnection.serverName.label=Jina ya server -poll.messages.count=Risala zitatumwa -default.number.format=0 -archive.activity.messages=Risala -action.cancel=Katisha -fconnection.test.message.sent=Risala ya jaribio imetumwa\! -default.button.search.label=Tafuta -quickmessage.phonenumber.label=Ongeza nambari ya simu\: -quickmessage.validation.prompt=Tafadhali jaza sehemu zote zinazohitajika -fmessage.quickmessage=Risala ya haraka -activity.name=Jina -autoreply.moreactions.export=Hamisha jibu la moja kwa moja -activities.create=Chagua kitendo kipya -default.boolean.false=Uongo -archive.activity.type=Aina -fmessage.hidepolldetails=Ficha grafu -import.contact.save.error=Kulikuwa na kosa katika kuwekwa kwa mwasiliani -smartgroup.startswith.label=inayoanza na -wizard.ok=Sawa -default.paginate.prev=Awali -fmessage.move.to.inbox=Kisanduku pokezi -traffic.filter.between.dates=Kati ya tarehe -emailfconnection.serverPort.label=Bandari ya server -quickmessage.details.label=Thibitisha vipengele -autoreply.not.saved=Jibu la moja kwa moja halijaweza kuwekwa -export.message.date.created=Tarehe ya kuundwa -autoreply.keyword.title=Panga risala za jibu la moja kwa moja ukitumia hili Neno Kuu\: -frontlinesms2.Poll.responses.validator.error.frontlinesms2.Poll.responses=Aina ya majibu haziwezi kuwa sawa -poll.details.label=Thibitisha vipengele -folder.create=Chagua folda mpya -tab.status=Hali -intellismsfconnection.username.label=Jina la mtumiaji -autoreply.moreactions.rename=Badilisha jina la jibu la moja kwa moja -connection.select=Chagua aina ya mwunganisho -quickmessage.enter.message=Andika risala -common.settings=Mazingira -connection.test.sent=Jaribio imetumwa kwa {0} na {1} na fanaka -emailfconnection.label=Barua pepe -export.contact.mobile=Nambari -export.contact.groups=Vikundi -emailfconnection.type.label=Aina -fmessage.archive.many=Hifadhi zote -export.csv=Fomati ya CSV itakayotumiwa na spreadsheet -quickmessage.recipient.label=Mpokeaji -fmessage.archive=Hifadhi -group.label=Kundi -wizard.send.message.title=Tuma Risala -fmessage.date.label=Tarehe -default.invalid.min.message=Tabia [{0}] ya darasa [{1}] iliyo na kima [{2}] ni chini ya kima kinachohitajika [{3}] -fmessage.label.multiple={0} risala -search.filter.activities=Tafuta kwa vitendo na folda -smallpopup.contact.delete.title=Futa -folder.label=Folda -default.search.moreoption.label=Chaguo zaidi -frontlinesms2.Autoreply.autoreplyText.nullable.error.frontlinesms2.Autoreply.autoreplyText=Risala haiwezi kuwa tupu -fmessage.trash.actions=Vitendo kuhusu takataka -contact.label=Mwasiliani -settings.logs.header=Mazingira > logi za mfumo -quickmessage.count.label=Hesabu ya Risala\: -poll.recipients.count=Wapokeaji wamechaguliwa -export.message.info=Kupakia risala kutoka FrontlineSMS, chagua aina ya upakiaji na ujumbe utakao ongezwa katika data inayopakiwa.. -default.button.create.label=Unda -quickmessage.confirm=Thibitisha -poll.recipients.label=Wapokeaji -fmessage.unarchive=Rejesha -contact.notinanygroup.label=Haiko kwa kikundi chochote -dynamicfield.contact_number.label=Nambari ya simu ya mwasiliani -fmessage.retry.many=Kurudia haikufaulu -quickmessage.selected.recipients=wapokeaji wamechaguliwa -autoreply.name.prompt=Ipe jina hii jibu ya moja kwa moja -magicwand.title=Tumia neno la ubadilifu -customfield.validation.prompt=Tafadhali jaza jina -customfield.validation.error=Jina limetumika -export.contact.notes=Madondo -connection.createtest.message.label=Risala -default.list.label={0} Orodha -clickatellfconnection.name.label=Jina -failed.pending.fmessages=Risala {0} zinazosuburi zimefeli. Enda kwenye sehemu ya risala zinazosuburi kuziona. -archive.folder.date=Tarehe -smslibfconnection.baud.label=Kiwango cha baud -default.created={0} imeundwa -autoreply.info.note=Kumbuka\: Ukihifadhi hili jibu la moja kwa moja, risala zijazo hazitapangwa na hili jibu la moja kwa moja. -smartgroup.info=Kuunda kundi , chagua amri zitakazofuatwa na Waasiliani ili wawe kwatika kikundi hiki -poll.messages.sent=risala {0} zimetumwa -flash.message.fmessage=risala {0} -import.message.complete=Risala {0} zimeingilizwa; {1} zikafeli -poll.title=Uchaguzi wa {0} -connection.send.test.message=Tuma risala kama majaribio -poll.confirm=Thibitisha -search.quickmessage=Tuma risala -poll.message.edit=Hariri risala itakayotumiwa mpokeaji -archive.inbox=Hifadhi kisanduku pokezi -contact.save.many=Weka zote -announcement.messages.count=risala zitatumwa -default.date.format=dd MMMM, yyyy hh\:mm -fmessage.resend=Tuma tena -export.message.source=Chanzo -poll.response=Orodha ya majibu -poll.sort.description=Watumaji risala watakapotuma majibu wakitumia neno kuu, FrontlineSMS inaweza kupanga risala hizo kulingana na neno hilo -contact.sent.messages=Risala zilizotumwa -export.message.destination=Kifiko -quickmessage.messages.count=risala zitatumwa -smallpopup.recipients.title=Wapokeaji -import.upload.failed=Kupakia kwa faili hiyo kumefeli -contact.email.label=Barua pepe -flash.smartgroup.delete.unable=Kikundi-erevu haukuweza kufutwa -fmessage.search.none=Hamna risala zilizopaikana -connection.header=Mazingira > Miunganisho > -default.invalid.email.message=Tabia [{0}] ya darasa [{1}] iliyo na kima [{2}] sio barua pepe halali -export.contacts.name3=Waasiliani wote (waasiliani {0}) -export.contacts.name2={0} kundi erevu (waasiliani {1}) -connection.route.destroyed=Njia imeunguka kutoka {0} mpaka {1} -export.contacts.name1={0} kundi ({1} waasiliani) -fmessage.number.error=Umeongeza maandishi yasiyo nambari hapa, utakapoweka maandishi yasiyo nambari yatatolewa -settings.general=Kwa ujumla -poll.pending.messages=Kutazama hadhi ya risala zako, fungua faili ya risala 'zinazosubiri'. -connection.test.message=Pongezi kutoka FrontlineSMS \\o/ umefaulu kuunda {0} kutuma risala \\o/ -default.delete.failed=Kufutwa kwa {0} iliyo na kitambulisho {1} -smartgroup.validation.prompt=Tafadhali jaza sehemu zote zinazohitajika. Unaweza kuchagua amri moja pekee katika sehemu yeyote -connection.type=Chagua aina -autoreply.blank.keyword=Neno Kuu tupu. Jibi litatumwa kwa risala zote zijazo -popup.activity.create=Unda Shughuli Mpya \: Chagua aina -contact.notes.label=Vidokezi -smallpopup.test.message.title=Risala jaribio -archive.folder.name=Jina -import.version1.info=Kuingiliza data kutoka version 1, yapakie kwa lugha ya Kiingereza -group.save.fail=Kundi halijawekwa vizuri -poll.edit.message=Hariri risala -system.notification.ok=Sawa -clickatellfconnection.label=Akaunti ya Clickatell -default.button.delete.confirm.message=Uko na hakika? -announcement.validation.prompt=Tafadhali jaza sehemu zote zinazohitajika -import.contact.complete=Waasiliani {0} wameinglizwa; {1} wamefeli -contact.groups.header=Vikundi -search.filter.messages.all=Zote zilizotumwa na kupokewa -searchdescriptor.only=, pekee {0} -clickatellfconnection.apiId.label=Kitambulisho cha API -action.back=Nyuma -group.delete.prompt=Uko na hakika ungetaka kufuta {0}? Ilani\: Hutaweza kuirejesha. -connectionstatus.connecting=Inaunganishwa -traffic.all.folders.activities=Onyesha matendo na faili zote -contact.save=Weka -contact.header=Waasiliani -default.blank.message=Tabia [{0}] ya darasa [{1}] haiwezi kuwachwa wazi -poll.prompt=Ipe jina uchaguzi huu -logs.filter.anytime=Saa zote -frontlinesms2.Poll.name.unique.error.frontlinesms2.Poll.name=Jina "{2}" lazima liwe lakipekee -announcement.delete.warn=Onyo la kufuta {0}\: Hutaweza kurejesha tangazo\! -poll.responses.prompt=Andika majibu unayotarajia (Kati ya 2 hadi 5) -export.pdf=Fomati ya PDF ili uweze kuchapisha -connection.route.failNotification=Imefeli kuunda njia katika {0} Hariri Muunganisho -poll.moreactions.edit=Hariri uchaguzi -default.search.date.format=d/M/yyyy -autoreply.message.count=nafasi 0 (Risala 1) -smallpopup.folder.title=Folda -tab.archive=Hifadhi -fconnection.unknown.type=Mwunganisho usiojulikana -customfield.name.label=Jina -action.ok=Sawa -search.header=Tafuta -status.connection.header=Miunganisho -autoreply.name.label=Risala -folder.title={0} folda -emailfconnection.password.label=nywila -typeMismatch.int=Ni lazima {0} iwe nambari halali -contact.edited.by.another.user=Mtumiaji mwingine amesasisha rekodi hii ulipokuwa ukihariri -announcement.recipients.count=wapokeaji waliochaguliwa -group.update.success=Kundi limesasishwa vizuri -logs.download.buttontext=Chukua logi -fmessage.selected.none=Hakuna risala iliyochaguliwa -fmessage.reply.many=Jibu zote -export.message.title=Kupakia kwa risala za FrontlineSMS -connection.route.create=Unda mwunganisho -announcement.recipients.label=Wapokeaji -import.contact.failed.download=Unda CSV ya waasiliani zilizofeli -autoreply.all.messages=Usitumie neno kuu (Risala zote zijazo zitapokea hili jibu la moja kwa moja) -message.character.count=Nafasi iliyobaki ni {0}. Risala {1} -message.character.count.warning=Idadi ya maneno inaweza kuongezeka baada ya kufanya mabadilisho -poll.sort.automatically=Panga risala zinazotumia hili neno kuu moja kwa moja -search.filter.inbox=Risala zilizopokewa pekee -download.logs.info2=watumiaji wengine wanaweza kuwa wameripoti shida inayolingana na wakapata suluhisho\! kuendelea na kutuma logi ulizonazo Tafadhali bonyeza 'Endelea' -download.logs.info1=Onyo\: Kundi la FrontlineSMS haliwezi kujibu maswali yako moja kwa moja. Ikiwa uko na maswali kuhusu utumiaji, Tafadhali angalia katika 'Msaada' ili uweze kupata majibu hapo. Usipofaulu, ripoti matatizo yako kwa usaidizi wa watumiaji\: -traffic.received=Imepokewa -default.notfound=Haikuweza kupata {0} na kitambulisho {0} -folder.unarchived.successfully=Folda imerejeshwa vizuri\! -tab.search=Tafuta -search.contact.name=Jina la mpokeaji -fmessage.footer.show.starred=Risala zenye nyota -settings.connections=Simu na miunganisho -many.selected={0} {1} zimechaguliwa -quickmessage.messages.label=Andika risala -poll.moreactions.delete=Futa uchaguzi -default.edit.label=Hariri {0} -group.name.label=Jina -searchdescriptor.from=, kutoka {0} -poll.question.yes.no=Swali linalo jibu 'ndio' au 'la' -contact.list.no.contact=Hakuna Waasiliani hapa\! -message.folder.header=Folda ya {0} -smslibfconnection.name.label=Jina -connectionstatus.not_connected=Haijaunganishwa -smartgroup.contains.label=iliyo na neno -poll.question.prompt=Andika swali -smartgroup.add.anotherrule=Ongeza amri nyingine -quickmessage.message.none=Hakuna -poll.moreactions.export=Pakia uchaguzi -poll.sort=Panga moja kwa moja -poll.alias=Malakabu -poll.aliases.prompt=Andika malakabu yanayolingana na majibu. -poll.aliases.prompt.details=Malakabu hayo yatenganishwe na koma. Lakabu ya kwanza litatumwa katika risala. -poll.alias.validation.error=Malakabu yafaa kuwa ya kipekee -announcement.info4=Kutazama tangazo, ibonyeze katika menyu iliyo pande ya kushoto -announcement.info3=Kutazama hadhi ya risala zako, fungua faili ya risala 'zinazosubiri' -announcement.info2=Itachukua muda kutuma risala zote, kutegemea na nambari ya risala zinazotumwa na mwunganisho wa mtandao -announcement.info1=Tangazo limeundwa na risala zikaongezwa kwenye foleni ya risala zitakazotumwa -poll.autoreply.label=Jibu la moja kwa moja -default.button.edit.label=Hariri -announcement.message.label=Risala -smslibfconnection.port.label=Bandari -poll.send.messages.none=Hamna risala zitakazotumwa -frontlinesms.welcome=Karibu FrontlineSMS\! \\o/ -contact.smartgroup.header=Vikundi-erevu -poll.question.multiple=Swali linalo majibu mengi (k.m 'Nyekundu', 'Samawati', 'Kijani Kibichi') -connection.name.autoconfigured=Modemu {0} {1} katika port {2} imeongezwa -poll.message.prompt=Risala hii itatumiwa wapokeaji wa uchaguzi huu -autoreply.confirm=Thibitisha -announcement.message.none=Hakuna -poll.moreactions.rename=Badilisha jina la uchaguzi -clickatellfconnection.username.label=jina la mtumiaji -intellismsfconnection.emailPassword.label=Nywila -announcement.saved=Tangazo limewekwa na risala zikaongezwa kwenye foleni ili zitumwe -wizard.send=Tuma -poll.no.automatic.sort=Usipange risala kulingana na neno kuu -group.delete=Futa kikundi -typeMismatch.java.lang.Short=Tabia {0} lazima iwe nambari halali -poll.recipients.none=Hakuna -contact.customfield.option.createnew=Unda mpya... -connection.confirm.header=Thibitisha mazingira -action.next=Ijayo -announcement.label=Tangazo -logs.filter.14days=Siku 14 zilizopita -contact.phonenumber.label=Nambari ya simu -contact.create=Unda mwasiliani mpya -logs.content=Risala -logs.filter.3days=Siku 3 zilizopita -traffic.allgroups=Onyesha vikundi vyote -settings.logs=Mfumo -traffic.filter.2weeks=Onyesha wiki mbili zilizopita -searchdescriptor.all.messages=Risala zote -poll.messages.queue.status=Itachukua muda kutuma risala zote, kutegemea na nambari ya risala zinazotumwa na mwunganisho wa mtandao. -autoreply.moreactions.edit=Hariri jibu la moja kwa moja -logs.filter.1day=Masaa 24 yaliyopita -default.new.label={0} mpya -fmessage.displayName.label=Jina -import.prompt=Chagua faili utakayoingiliza -contact.name.label=Jina -announcement.prompt=Ipe jina tangazo hili -autoreply.text.none=Hamna -fmessage.text.label=Risala -import.backup.label=Ingiliza data kutoka hifadhi ya hapo mbeleni -flash.message.activity.found.not=Kitendo hakijaweza kupatikana -archive.folder.none=  Hakuna faili zilizohifadhiwa -contact.create.smartgroup=Unda kikundi-erevu kipya -smslibfconnection.label=Simu/Modemu -connection.delete=Futa muunganisho -smallpopup.delete.prompt=Futa {0} ? -emailfconnection.name.label=Jina -action.done=Maliza -fmessage.header=Risala -default.show.label=Onyesha {0} -poll.description=Tuma swali na uchambue majibu -poll.reply.text=Jibu "{0} {1}" kujibu Ndio, "{2} {3}" kujibu La. -search.result.header=Matokeo -announcement.confirm.message=Thibitisha risala -group.rename=Badilisha Jina la kikundi -default.button.update.label=Sasisha -popup.title.saved={0} imewekwa\! -intellismsfconnection.send.label=Tumia kutuma risala -default.unarchived=Tohifadhi -announcement.moreactions.edit=Hariri tangazo -fmessage.many=Risala -default.button.delete.label=Futa -default.deleted.message={0} imefutwa -fmessage.reply=Jibu -smslibFconnection.send.validator.error.send=Modemu yafaa kutumika kutuma -smslibFconnection.receive.validator.error.receive=au kupokea risala - +fconnection.name=Fconnection +fconnection.unknown.type=Unknown connection type: +fconnection.test.message.sent=Test message queued for sending! +announcement.saved=Your Announcement has been saved and message(s) have been queued to send. +announcement.not.saved=Your Announcement could not be saved. +announcement.save.success={0} Announcement has been saved! +announcement.id.exist.not=Could not find announcement with id {0} +autoreply.save.success={0} Autoreply has been saved! +report.creation.error=Error creating report +export.message.title=Message Export +export.database.id=DatabaseID +export.message.text=Text +export.message.destination.name=Destination Name +export.message.destination.mobile=Destination Mobile +export.message.source.name=Source Name +export.message.source.mobile=Source Mobile +export.contact.title=Contact Export +export.contact.name=Name +export.contact.mobile=Mobile +export.contact.email=Email +export.contact.notes=Notes +export.contact.groups=Groups +export.messages.name1={0} {1} ({2} messages) +export.messages.name2={0} ({1} messages) +export.contacts.name1={0} group ({1} contacts) +export.contacts.name2={0} smart group ({1} contacts) +export.contacts.name3=All contacts ({0} contacts) +folder.archived.successfully=Folder was archived successfully! +folder.unarchived.successfully=Folder was unarchived successfully! +folder.trashed=Folder has been trashed! +folder.restored=Folder has been restored! +folder.exist.not=Could not find folder with id {0} +folder.renamed=Folder Renamed +group.label=Group +group.name.label=Name +group.update.success=Group updated successfully. +group.save.fail=Group save failed. +group.delete.fail=Unable to delete group. In use by a subscription activity. +import.label=Import contacts and messages +import.backup.label=You can use the form below to import your contacts. You can also import messages that you have from a previous Frontline project. +import.prompt.type=Select the type of data you wish to import before uploading +import.messages=Messages in Frontline's CSV format +import.prompt=Select the file containing your data to initiate the import +import.upload.failed=File upload has failed for some reason. +import.contact.save.error=An error was encountered whilst saving the contact. +import.contact.complete=Congratulations! {0} contacts were successfully imported. +import.message.save.error=Encountered error saving message +import.message.complete={0} messages were imported; {1} failed +many.selected={0} {1}s selected +flash.message.activity.found.not=Activity could not be found. +flash.message.folder.found.not=Folder could not be found. +flash.message=Message +flash.message.fmessage={0} message(s) +flash.message.fmessages.many={0} SMS messages +flash.message.fmessages.many.one=1 SMS message +fmessage.exist.not=Could not find message with id {0} +flash.message.poll.queued=Your Poll has been saved and messages are queued to send. +flash.message.poll.not.saved=Poll could not be saved! +system.notification.ok=OK +system.notification.fail=FAIL +flash.smartgroup.delete.unable=Unable to delete Smart Group +flash.smartgroup.saved=Smart Group {0} saved +flash.smartgroup.save.failed=Smart Group save failed. Errors were {0} +smartgroup.id.exist.not=Could not find smart group with id {0} +smartgroup.save.failed=Failed to save smart group{0}with params {1}{2}errors: {3} +searchdescriptor.searching=Searching +searchdescriptor.all.messages=all messages +searchdescriptor.archived.messages=, including archived messages +searchdescriptor.exclude.archived.messages=, without archived messages +searchdescriptor.only=, only {0} +searchdescriptor.between=, between {0} and {1} +searchdescriptor.from=, from {0} +searchdescriptor.until=, until {0} +poll.title={0} +announcement.title={0} +autoreply.title={0} +folder.title={0} +frontlinesms.welcome=Welcome to FrontlineSMS! \\o/ +failed.pending.fmessages={0} pending message(s) failed. Go to pending messages section to view. +subscription.title={0} +subscription.info.group=Group: {0} +subscription.info.groupMemberCount={0} members +subscription.info.keyword=Top-level keywords: {0} +subscription.sorting.disable=Disable automatic sorting +subscription.info.joinKeywords=Join: {0} +subscription.info.leaveKeywords=Leave: {0} +subscription.group.goto=View Group +subscription.group.required.error=Subscriptions must have a group +subscription.save.success={0} Subscription has been saved! +language.label=Language +language.prompt=Change the language of the user interface. +frontlinesms.user.support=User Support +download.logs.info1=WARNING: The Frontline Team is unable to respond directly to submitted logs. If you have a user support request, please check the Help files to see whether you can find the answer there. If not, report your issue via our user support forums: +download.logs.info2=Other users may even have reported the same problem and found a solution! To carry on and submit your logs, please click 'Continue'. +# Configuration location info +configuration.location.title=Configuration Location +configuration.location.instructions=You can find your application configuration at: +dynamicfield.contact_name.label=Contact Name +dynamicfield.contact_number.label=Contact Number +dynamicfield.keyword.label=Keyword +dynamicfield.message_content.label=Message Content +# TextMessage domain +fmessage.queued=Message has been queued to send to {0}. +fmessage.queued.multiple=Message has been queued to send to {0} recipients. +fmessage.retry.success=Message has been re-queued to send to {0}. +fmessage.retry.success.multiple={0} message(s) have been re-queued for sending. +fmessage.displayName.label=Name +fmessage.text.label=Message +fmessage.date.label=Date +fmessage.to=To: {0} +fmessage.to.multiple=To: {0} recipients +fmessage.quickmessage=Send message +fmessage.archive=Archive +fmessage.activity.archive=Archive {0} +fmessage.unarchive=Unarchive {0} +fmessage.export=Export +fmessage.rename=Rename {0} +fmessage.edit=Edit {0} +fmessage.delete=Delete +fmessage.moreactions=More actions... +fmessage.footer.show=Show +fmessage.footer.show.failed=Failed +fmessage.footer.show.all=All +fmessage.footer.show.starred=Starred +fmessage.footer.show.incoming=Incoming +fmessage.footer.show.outgoing=Outgoing +fmessage.archive.back=Back +fmessage.activity.sentmessage=({0} messages sent) +fmessage.failed=failed +fmessage.header=messages +fmessage.section.inbox=Inbox +fmessage.section.sent=Sent +fmessage.section.pending=Pending +fmessage.section.trash=Trash +fmessage.addsender=Add to contacts +fmessage.resend=Resend +fmessage.retry=Retry +fmessage.reply=Reply +fmessage.forward=Forward +fmessage.messages.none=No messages have been received yet. +fmessage.selected.none=No message selected. +fmessage.move.to.header=Move message to... +fmessage.archive.many=Archive selected +fmessage.count=1 message +fmessage.count.many={0} messages +fmessage.many=messages +fmessage.delete.many=Delete selected +fmessage.reply.many=Reply selected +fmessage.restore=Restore +fmessage.restore.many=Restore +fmessage.retry.many=Retry selected +fmessage.selected.many={0} messages selected +fmessage.unarchive.many=Unarchive selected +# TODO move to poll.* +fmessage.showpolldetails=Show graph +fmessage.hidepolldetails=Hide graph +# TODO move to search.* +fmessage.search.none=No messages found +fmessage.search.description=Start new search on the left +fmessage.connection.receivedon=Received on: {0} +activity.name=Name +activity.delete.prompt=Move {0} to trash. This will transfer all associated messages to the trash section. +activity.label=Activity +activity.categorize=Categorize response +magicwand.title=Add substitution expressions +folder.create.success=Folder create successfully +folder.create.failed=Could not create folder +folder.name.validator.error=Folder name already in use +folder.name.blank.error=Folder name cannot be blank +poll.name.blank.error=Activity name cannot be blank +poll.name.validator.error=Activity name already in use +autoreply.name.blank.error=Activity name cannot be blank +autoreply.name.validator.error=Activity name already in use +announcement.name.blank.error=Activity name cannot be blank +announcement.name.validator.error=Activity name already in use +group.name.blank.error=Group name cannot be blank +group.name.validator.error=Group name already in use #Jquery Validation messages -jquery.validation.required=Nafasi hii yahitajika. -jquery.validation.remote=Tafadhali jaza nafasi hii. -jquery.validation.email=Tafadhali jaza barua pepe halisi. -jquery.validation.url=Tafadhali jaza URL. -jquery.validation.date=Tafadhali jaza siku halisi. -jquery.validation.dateISO=Tafadhali jaza siku(ISO) halisi. -jquery.validation.number=Tafadhali jaza nambari. -jquery.validation.digits=Tafadhali jaza nambari pekee. -jquery.validation.creditcard=Tafadhali jaza nambari halisi. -jquery.validation.equalto=Tafadhali jaza nafasi hii na neno sawa. -jquery.validation.accept=Tafadhali jaza nafasi hii na neno halisi. -jquery.validation.maxlength=Usijaze zaidi ya maneno {0}. -jquery.validation.minlength=Jaza maneno ya ziada. -jquery.validation.rangelength=Tafadhali jaza neno lenye urefu baina ya maneno {0} na {1}. -jquery.validation.range=Tafadhali jaza nambari kati ya {0} na {1}. -jquery.validation.max=Tafadhali jaza nambari isiyozidi {0}. -jquery.validation.min=Tafadhali jaza nambari kubwa kuliko {0}. - -webconnection.title=Uhusiano wa mtandao wa {0} -webconnection.label=Uhusiano wa mtandao -webconnection.description=Husiana na mtandao -webconnection.sorting=Panga moja kwa moja -webconnection.configure=Sanidi huduma -webconnection.confirm=Thibitisha -webconnection.keyword.title=Weka risala zote zinazolingana na neno kuu hii: -webconnection.all.messages=Haitatumia neno kuu. Risala zote zitawekwa katika Uhusiano wa mtandao huu. -webconnection.httpMethod.label=Chagua itafaki ya HTTP: -webconnection.httpMethod.get=GET -webconnection.httpMethod.post=POST -webconnection.name.prompt=Ipe Uhusiano wa mtandao jina -webconnection.details.label=Thibitisha melezo -webconnection.parameters=Sanidi data itakayotumwa kwenye server -webconnection.parameters.confirm=Dhibitisha data iliyotumwa kwenye server -webconnection.keyword.label=Nemo Kuu: -webconnection.none.label=Hakuna -webconnection.url.label=Url ya Kompyuta Tumishi: -webconnection.param.name=Jina: -webconnection.param.value=Thamani: -webconnection.add.anotherparam=Ongeza elezo -dynamicfield.message_body.label=Nakala ya Risala -dynamicfield.message_body_with_keyword.label=Nakala ya Risala isiyo na neno kuu -dynamicfield.message_src_number.label=Nambari ya Mwasili -dynamicfield.message_src_name.label=Jina la Mwasili -dynamicfield.message_timestamp.label=Wakati wa Risala -webconnection.keyword.validation.error=Neno kuu yahitajika -webconnection.url.validation.error=Url yahitajika -webconnection.save=Uhusiano wa mtandao umewekwa! - -webconnection.moreactions.delete=Toa Uhusiano wa mtandao -webconnection.moreactions.rename=Badilisha jina ya Uhusiano wa mtandao -webconnection.moreactions.edit=Hariri Uhusiano wa mtandao -webconnection.moreactions.export=Hamisha Uhusiano wa mtandao - +jquery.validation.required=This field is required. +jquery.validation.remote=Please fix this field. +jquery.validation.email=Please enter a valid email address. +jquery.validation.url=Please enter a valid URL. +jquery.validation.date=Please enter a valid date. +jquery.validation.dateISO=Please enter a valid date (ISO). +jquery.validation.number=Please enter a valid number. +jquery.validation.digits=Please enter only digits. +jquery.validation.creditcard=Please enter a valid credit card number. +jquery.validation.equalto=Please enter the same value again. +jquery.validation.accept=Please enter a value with a valid extension. +jquery.validation.maxlength=Please enter no more than {0} characters. +jquery.validation.minlength=Please enter at least {0} characters. +jquery.validation.rangelength=Please enter a value between {0} and {1} characters long. +jquery.validation.range=Please enter a value between {0} and {1}. +jquery.validation.max=Please enter a value less than or equal to {0}. +jquery.validation.min=Please enter a value greater than or equal to {0}. +# Webconnection common +webconnection.select.type=Select Web Service or application to connect to: +webconnection.type=Select Type +webconnection.title={0} +webconnection.label=Web Connection +webconnection.description=Connect to web service. +webconnection.sorting=Automatic sorting +webconnection.configure=Configure service +webconnection.api=Fichua API +webconnection.api.info=FrontlineSMS can be configured to receive incoming requests from your remote service and trigger outgoing messages. For more details, see the Web Connection help section. +webconnection.api.enable.label=Enable API +webconnection.api.secret.label=API secret key: +webconnection.api.disabled=API disabled +webconnection.api.url=API URL +# Webconnection - generic +webconnection.generic.label=Other web service +webconnection.generic.description=Send messages to other web service +webconnection.generic.subtitle=HTTP Web Connection +# Webconnection - Ushahidi/Crowdmap +webconnection.ushahidi.label=Crowdmap / Ushahidi +webconnection.ushahidi.description=Send messages to Crowdmap or to an Ushahidi server. +webconnection.ushahidi.key.description=The API key for either Crowdmap or Ushahidi can be found in the Settings on the Crowdmap or Ushahidi web site. +webconnection.ushahidi.url.label=Ushahidi deployment address: +webconnection.ushahidi.key.label=Ushahidi API Key: +webconnection.crowdmap.url.label=Crowdmap deployment address: +webconnection.crowdmap.key.label=Crowdmap API Key: +webconnection.ushahidi.serviceType.label=Select Service +webconnection.ushahidi.serviceType.crowdmap=Crowdmap +webconnection.ushahidi.serviceType.ushahidi=Ushahidi +webconnection.crowdmap.url.suffix.label=.crowdmap.com +webconnection.ushahidi.subtitle=Web Connection to {0} +webconnection.ushahidi.service.label=Service: +webconnection.ushahidi.fsmskey.label=Frontline API Secret: +webconnection.ushahidi.crowdmapkey.label=Crowdmap/Ushahidi API Key: +webconnection.ushahidi.keyword.label=Keyword: +webconnection.confirm=Confirm +webconnection.keyword.title=Transfer every message received containing the following keyword: +webconnection.all.messages=Do not use keyword (All incoming messages will be forwarded to this web Connection +webconnection.httpMethod.label=Select HTTP Method: +webconnection.httpMethod.get=PATA +webconnection.httpMethod.post=CHAPISHA +webconnection.name.prompt=Name this web connection +webconnection.details.label=Confirm details +webconnection.parameters=Configure information sent to the server +webconnection.parameters.confirm=Configured information sent to the server +webconnection.keyword.label=Keyword: +webconnection.none.label=None +webconnection.url.label=Enter your URL* +webconnection.param.name=Name* +webconnection.param.value=Value +webconnection.add.anotherparam=Add a new parameter to send to URL +dynamicfield.message_body.label=Message Text +dynamicfield.message_body_with_keyword.label=Message Text With keyword +dynamicfield.message_src_number.label=Contact number +dynamicfield.message_src_name.label=Contact name +dynamicfield.message_timestamp.label=Message Timestamp +webconnection.keyword.validation.error=Keyword is required +webconnection.url.validation.error=URL is required +webconnection.save=The Web Connection has been saved! +webconnection.saved=Web Connection saved! +webconnection.save.success={0} Web Connection has been saved! +webconnection.generic.service.label=Service: +webconnection.generic.httpMethod.label=HTTP Method: +webconnection.generic.url.label=Address: +webconnection.generic.keyword.label=Keyword: +webconnection.generic.parameters.label=Configured information sent to the server: +webconnection.generic.key.label=API Key: +frontlinesms2.Keyword.value.validator.error.frontlinesms2.UshahidiWebconnection.keyword.value=Invalid keyword Value #Subscription i18n -subscription.title=Usajili wa {0} -subscription.info.group=Kikundi: {0} -subscription.info.groupMemberCount=wawasiliani {0} -subscription.info.keyword=Neno Kuu: {0} -subscription.info.joinAliases=Kuingia: {0} -subscription.info.leaveAliases=Kutoka: {0} -subscription.group.goto=Tazama Kukundi -subscription.label=Usajili -subscription.name.prompt=Patia Usajili Jina -subscription.details.label=Thibitisha maelezo -subscription.description=Ingia kikundi kwa kutumia Risala -subscription.select.group.keyword=Kikundi na Neno kuu -subscription.select.group=Chagua kikundi cha usajili -subscription.group.none.selected=Chagua kikundi -subscription.autoreplies=Majibu ya moja kwa moja -subscription.aliases=Lakabu -subscription.confirm=Thibitisha -subscription.group.header=Chagua Kikundi -subscription.group.description=Mwasiliani aweza ongezwa am kutolewa kwa kikundi wakati FrontlineSMS itapata risala iliyo na neno kuu -subscription.keyword.header=Neno Kuu -subscription.aliases.header=Lakabu -subscription.default.action.header=Kitendo Msingi -subscription.default.action.description=Wakati risala yafanana na neno kuu lakini hayafanani na lakabu zake, moja ya haya yatatendeka -subscription.aliases.leave=Lakabu za kutoka kkikundi -subscription.aliases.join=Lakabu za kuingia kikundi -subscription.default.action.join=Ongeza mwasiliani kwa kikundi -subscription.default.action.leave=Toa mwasiliani kwa kikundi -subscription.default.action.toggle=Ongeza ama Toa mwasiliani kutoka kwa kikundi -subscription.autoreply.join=Tuma risala wakati mwasiliani aingia kukundi -subscription.autoreply.leave=Tuma risala wakati mwasiliani atoka kukundi -subscription.confirm.group=Kikundi -subscription.confirm.keyword=Neno Kuu -subscription.confirm.join.alias=Lakabu za kuingia -subscription.confirm.leave.alias=Lakabu za kutoka -subscription.confirm.default.action=Kitendo Msingi -subscription.confirm.join.autoreply=Majibu ya moja kwa moja ya kuingia -subscription.confirm.leave.autoreply=Majibu ya moja kwa moja ya kutoka -subscription.info1=Usajili imeundwa na inafanya kazi -subscription.info2=Risala zinazolingana na neno kuu zitabadilisha wawasiliani wa kikundi -subscription.info3=Kuona usajili, ichague kutoka sehemu iliyo mkono wa kushoto -subscription.categorise.title=Ingia ama Toka kwa Usajili -subscription.categorise.info=Tafathali chagua kitendo utakachofanya kwa usajili kwa wawasiliani waliochaguliwa watakapo ongezwa kwa {0} -subscription.categorise.join.label=Ongeza wawasiliani wote kwa {0} -subscription.categorise.leave.label=Toa wawasiliani wote kutoka {0} -subscription.categorise.toggle.label=Ongezea ama Toa wawasiliani kutoka {0} -subscription.join=Ingia -subscription.leave=Toka - -subscription.keyword.required=Neno kuu yahitajika -subscription.jointext.required=Risala yakuingia yahitajika -subscription.leavetext.required=Risala yakutoka yahitajika -subscription.moreactions.delete=Toa usajili -subscription.moreactions.rename=Badilisha jina ya usajili -subscription.moreactions.edit=Hariri usajili -subscription.moreactions.export=Hamisha usajili -subscription.group.required.error=Usajili lazime iwe na kikundi - +subscription.label=Subscription +subscription.name.prompt=Name this Subscription +subscription.details.label=Confirm details +subscription.description=Allow people to automatically join and leave contact groups using a message keyword +subscription.select.group=Select the group for the subscription +subscription.group.none.selected=Select group +subscription.autoreplies=Autoreplies +subscription.sorting=Automatic sorting +subscription.sorting.header=Process messages automatically using a keyword (optional) +subscription.confirm=Confirm +subscription.keyword.header=Enter keywords for this subscription +subscription.group.header=Select Group +subscription.group.description=Contacts can be added and removed from groups automatically when Frontline receives a message that includes a special keyword. +subscription.top.keyword.description=Enter the top-level keywords that will sort messages into this activity and apply the subscription action. +subscription.top.keyword.more.description=You may enter multiple top-level keywords for each option, separated with commas. Top-level keywords need to be unique across all activities. +subscription.keywords.header=Enter keywords for joining and leaving this group. +subscription.keywords.description=You may enter multiple keywords separated by commas. If no top-level keywords are entered above, then these join and leave keywords need to be unique across all activities. +subscription.default.action.header=Select an action when no keywords sent +subscription.default.action.description=Select the desired action when a message matches the top-level keyword but none of the join or leave keywords: +subscription.keywords.leave=Leave keyword(s) +subscription.keywords.join=Join keyword(s) +subscription.default.action.join=Add the contact to the group +subscription.default.action.leave=Remove the contact from the group +subscription.default.action.toggle=Toggle the contact's group membership +subscription.autoreply.join=Send an automatic reply when a contact joins the group +subscription.autoreply.leave=Send an automatic reply when a contact leaves the group +subscription.confirm.group=Group +subscription.confirm.keyword=Keyword +subscription.confirm.join.alias=Join Keywords +subscription.confirm.leave.alias=Leave Keywords +subscription.confirm.default.action=Default Action +subscription.confirm.join.autoreply=Join Autoreply +subscription.confirm.leave.autoreply=Leave Autoreply +subscription.info1=The subscription has been saved and is now active +subscription.info2=Incoming messages that match this keyword will now change contacts' group membership as defined +subscription.info3=To see the subscription, click on it in the left-hand menu +subscription.categorise.title=Categorise messages +subscription.categorise.info=Please select the action to be performed with the senders of the selected message(s) when they are added to {0} +subscription.categorise.join.label=Add senders to {0} +subscription.categorise.leave.label=Remove senders from {0} +subscription.categorise.toggle.label=Toggle senders' membership of {0} +subscription.join=Join +subscription.leave=Leave +subscription.sorting.example.toplevel=e.g SOLUTION +subscription.sorting.example.join=e.g SUBSCRIBE, JOIN +subscription.sorting.example.leave=e.g UNSUBSCRIBE, LEAVE +subscription.keyword.required=Keyword is required +subscription.jointext.required=Please enter join autoreply text +subscription.leavetext.required=Please enter leave autoreply text +subscription.moreactions.delete=Delete subscription +subscription.moreactions.rename=Rename subscription +subscription.moreactions.edit=Edit subscription +subscription.moreactions.export=Export subscription +# Generic activity sorting +activity.generic.sorting=Automatic processing +activity.generic.sorting.subtitle=Process messages automatically using a keyword (optional) +activity.generic.sort.header=Process messages automatically using a keyword (optional) +activity.generic.sort.description=If people send in messages beginning with a particular keyword, Frontline products can automatically process the messages on your system. +activity.generic.keywords.title=Enter keywords for activity. You can enter multiple keywords separated by commas: +activity.generic.keywords.subtitle=Enter keywords for the activity +activity.generic.keywords.info=You can enter multiple keywords separated by commas: +activity.generic.no.keywords.title=Do not use a keyword +activity.generic.no.keywords.description=All incoming messages that do not match any other keywords will trigger this activity +activity.generic.disable.sorting=Do not automatically sort messages +activity.generic.disable.sorting.description=Messages will not be automatically processed by this activity +activity.generic.enable.sorting=Process responses containing a keyword automatically +activity.generic.sort.validation.unique.error=Keywords must be unique +activity.generic.keyword.in.use=The keyword {0} is already in use by activity {1} +activity.generic.global.keyword.in.use=Activity {0} is set to receive all messages that do not match other keywords. You can only have one activity at a time with this setting #basic authentication -basic.authentication=Uthibitishaji wa Kimsingi -basic.authentication.label=Jina la mtumiaji na nywila zinahitajika kutumia FrontlineSMS kwenye mtandao -basic.authentication.enable=Wezesha Uthibitishaji wa Kimsingi -basic.authentication.username=Jina la Mtumiaji -basic.authentication.password=Nywila -basic.authentication.confirm.password=Thibitisha Nywila -basic.authentication.password.mismatch=Nywila hazifanani +auth.basic.label=Basic Authentication +auth.basic.info=Require a username and password for accessing FrontlineSMS across the network +auth.basic.password.mismatch=Passwords don't match +newfeatures.popup.title=New Features +newfeatures.popup.showinfuture=Show this dialog in future +dynamicfield.message_text.label=Message text +dynamicfield.message_text_with_keyword.label=Message Text with Keyword +dynamicfield.sender_name.label=Sender Name +dynamicfield.sender_number.label=Sender Number +dynamicfield.recipient_number.label=Recipient Number +dynamicfield.recipient_name.label=Recipient Name diff --git a/plugins/frontlinesms-core/grails-app/jobs/frontlinesms2/ContactImportJob.groovy b/plugins/frontlinesms-core/grails-app/jobs/frontlinesms2/ContactImportJob.groovy new file mode 100644 index 000000000..b3dfd8290 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/jobs/frontlinesms2/ContactImportJob.groovy @@ -0,0 +1,20 @@ +package frontlinesms2 + +class ContactImportJob { + def grailsApplication + + def execute(context) { + def contactImportService = grailsApplication.mainContext.contactImportService + def jobData = context.mergedJobDataMap + def fileType = jobData.get('fileType') + def params = jobData.get('params') + def request = jobData.get('request') + + if(fileType == 'csv') { + contactImportService.importContactCsv(params, request) + } else { + contactImportService.importContactVcard(params, request) + } + } + +} diff --git a/plugins/frontlinesms-core/grails-app/jobs/frontlinesms2/EnableFconnectionJob.groovy b/plugins/frontlinesms-core/grails-app/jobs/frontlinesms2/EnableFconnectionJob.groovy new file mode 100644 index 000000000..f81373ae3 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/jobs/frontlinesms2/EnableFconnectionJob.groovy @@ -0,0 +1,12 @@ +package frontlinesms2 + +class EnableFconnectionJob { + // TODO can't we just inject the service we want here? + def grailsApplication + + def execute(context) { + def connection = Fconnection.get(context.mergedJobDataMap.get('connectionId').toLong()) + grailsApplication.mainContext.fconnectionService.enableFconnection(connection) + } +} + diff --git a/plugins/frontlinesms-core/grails-app/jobs/frontlinesms2/MessageSendJob.groovy b/plugins/frontlinesms-core/grails-app/jobs/frontlinesms2/MessageSendJob.groovy index 3a161c756..7f47121a9 100644 --- a/plugins/frontlinesms-core/grails-app/jobs/frontlinesms2/MessageSendJob.groovy +++ b/plugins/frontlinesms-core/grails-app/jobs/frontlinesms2/MessageSendJob.groovy @@ -5,14 +5,14 @@ class MessageSendJob { def execute(context) { def ids = context.mergedJobDataMap.get('ids') - def messages = Fmessage.getAll(ids) + def messages = TextMessage.getAll(ids) messages.each { m -> messageSendService.send(m) } } /** Send a message or messages in 30 seconds time */ - static defer(Fmessage message) { + static defer(TextMessage message) { defer([message]) } diff --git a/plugins/frontlinesms-core/grails-app/jobs/frontlinesms2/CreateRouteJob.groovy b/plugins/frontlinesms-core/grails-app/jobs/frontlinesms2/ReportSmssyncTimeoutJob.groovy similarity index 61% rename from plugins/frontlinesms-core/grails-app/jobs/frontlinesms2/CreateRouteJob.groovy rename to plugins/frontlinesms-core/grails-app/jobs/frontlinesms2/ReportSmssyncTimeoutJob.groovy index 66388b809..2ed8e7476 100644 --- a/plugins/frontlinesms-core/grails-app/jobs/frontlinesms2/CreateRouteJob.groovy +++ b/plugins/frontlinesms-core/grails-app/jobs/frontlinesms2/ReportSmssyncTimeoutJob.groovy @@ -1,10 +1,10 @@ package frontlinesms2 -class CreateRouteJob { +class ReportSmssyncTimeoutJob { def grailsApplication def execute(context) { def connection = Fconnection.get(context.mergedJobDataMap.get('connectionId').toLong()) - grailsApplication.mainContext.fconnectionService.createRoutes(connection) + grailsApplication.mainContext.smssyncService.reportTimeout(connection) } } diff --git a/plugins/frontlinesms-core/grails-app/jobs/frontlinesms2/UploadRegistrationDataJob.groovy b/plugins/frontlinesms-core/grails-app/jobs/frontlinesms2/UploadRegistrationDataJob.groovy new file mode 100644 index 000000000..eb8f7700b --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/jobs/frontlinesms2/UploadRegistrationDataJob.groovy @@ -0,0 +1,62 @@ +package frontlinesms2 + +class UploadRegistrationDataJob { + + File regPropFile + def dataUploadService + + static final String UPLOAD_URL = "http://register.frontlinesms.com/process/" + static final String REGISTRATION_FILE = 'registration.properties' + + + static triggers = { + long week = 7 * 24 * 3600 * 1000 // execute job once in 7 days + simple name:'RegistrationUpload', startDelay:0, repeatInterval:week, repeatCount:1 + } + + def execute() { + println "UploadRegistrationDataJob: Attempting to upload registration data..." + Properties properties = getRegistrationProperties() + if(!properties || properties?.isEmpty()){ + println "SKIPPED : Registration data not available!" + return //there is no registration data to send + } + def dataMap = convertPropertiestoMap(properties) + def registered = (dataMap['registered'] == 'true')?:false + if(registered) { + println "SKIPPED : Registration data has already been uploaded!" + return //registration data has already been uploaded + } + try{ + boolean success = dataUploadService.upload(UPLOAD_URL,dataMap) + if(!success){ + println "FAILED : Registration data upload NOT successful, check your Internet connection!" + return + } + writeRegistrationPropertiesFile(properties) + println "SUCCESS : Successfully uploaded registration data!" + }catch(Exception e){ + println "FAILED : Registration data upload NOT successful, check your Internet connection!" + } + } + + def getRegistrationProperties() { + regPropFile = new File(ResourceUtils.resourceDirectory, REGISTRATION_FILE) + if(!regPropFile.exists()) + return null + Properties properties = new Properties() + regPropFile.withInputStream { stream -> properties.load(stream) } + properties + } + + def writeRegistrationPropertiesFile(Properties properties) { + properties.setProperty("registered",'true'); + properties.store(new OutputStreamWriter(new FileOutputStream(regPropFile), "UTF-8"),null); + } + + def convertPropertiestoMap(Properties p) { + def m = [:] + p.each { k, v -> m[k] = v } + return m + } +} diff --git a/plugins/frontlinesms-core/grails-app/migrations/changelog-2.0.groovy b/plugins/frontlinesms-core/grails-app/migrations/changelog-2.0.groovy new file mode 100644 index 000000000..9ef54282d --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/migrations/changelog-2.0.groovy @@ -0,0 +1,320 @@ +databaseChangeLog = { + + changeSet(author: "geoffrey (generated)", id: "1355230052153-1") { + createTable(tableName: "autoforward") { + column(name: "id", type: "bigint") { + constraints(nullable: "false", primaryKey: "true", primaryKeyName: "autoforwardPK") + } + } + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-2") { + createTable(tableName: "autoforward_contact") { + column(name: "autoforward_contacts_id", type: "bigint") + + column(name: "contact_id", type: "bigint") + } + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-3") { + createTable(tableName: "autoforward_grup") { + column(name: "autoforward_groups_id", type: "bigint") + + column(name: "group_id", type: "bigint") + } + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-4") { + createTable(tableName: "autoforward_smart_group") { + column(name: "autoforward_smart_groups_id", type: "bigint") + + column(name: "smart_group_id", type: "bigint") + } + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-5") { + createTable(tableName: "fconnection_fmessage") { + column(name: "fconnection_messages_id", type: "bigint") + + column(name: "fmessage_id", type: "bigint") + } + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-6") { + createTable(tableName: "generic_webconnection") { + column(name: "id", type: "bigint") { + constraints(nullable: "false", primaryKey: "true", primaryKeyName: "generic_webcoPK") + } + } + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-7") { + createTable(tableName: "request_parameter") { + column(autoIncrement: "true", name: "id", type: "bigint") { + constraints(nullable: "false", primaryKey: "true", primaryKeyName: "request_paramPK") + } + + column(name: "version", type: "bigint") { + constraints(nullable: "false") + } + + column(name: "connection_id", type: "bigint") { + constraints(nullable: "false") + } + + column(name: "name", type: "varchar(255)") { + constraints(nullable: "false") + } + + column(name: "value", type: "varchar(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-8") { + createTable(tableName: "smssync_dispatch") { + column(name: "connection_id", type: "bigint") { + constraints(nullable: "false") + } + + column(name: "dispatch_id", type: "bigint") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-9") { + createTable(tableName: "smssync_fconnection") { + column(name: "id", type: "bigint") { + constraints(nullable: "false", primaryKey: "true", primaryKeyName: "smssync_fconnPK") + } + + column(name: "receive_enabled", type: "boolean") { + constraints(nullable: "false") + } + + column(name: "secret", type: "varchar(255)") + + column(name: "send_enabled", type: "boolean") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-10") { + createTable(tableName: "subscription") { + column(name: "id", type: "bigint") { + constraints(nullable: "false", primaryKey: "true", primaryKeyName: "subscriptionPK") + } + + column(name: "default_action", type: "varchar(255)") { + constraints(nullable: "false") + } + + column(name: "group_id", type: "bigint") { + constraints(nullable: "false") + } + + column(name: "join_autoreply_text", type: "varchar(255)") + + column(name: "leave_autoreply_text", type: "varchar(255)") + } + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-11") { + createTable(tableName: "ushahidi_webconnection") { + column(name: "id", type: "bigint") { + constraints(nullable: "false", primaryKey: "true", primaryKeyName: "ushahidi_webcPK") + } + } + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-12") { + createTable(tableName: "webconnection") { + column(name: "id", type: "bigint") { + constraints(nullable: "false", primaryKey: "true", primaryKeyName: "webconnectionPK") + } + + column(name: "api_enabled", type: "boolean") { + constraints(nullable: "false") + } + + column(name: "http_method", type: "varchar(255)") { + constraints(nullable: "false") + } + + column(name: "secret", type: "varchar(255)") + + column(name: "url", type: "varchar(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-13") { + addColumn(tableName: "clickatell_fconnection") { + column(name: "from_number", type: "varchar(255)") + } + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-14") { + addColumn(tableName: "clickatell_fconnection") { + column(name: "send_to_usa", type: "boolean") + } + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-15") { + addColumn(tableName: "fmessage") { + column(name: "owner_detail", type: "varchar(255)") + } + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-16") { + //Not null contraint is added at the end of this script + addColumn(tableName: "keyword") { + column(name: "is_top_level", type: "boolean") + } + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-17") { + addColumn(tableName: "keyword") { + column(name: "keywords_idx", type: "integer") + } + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-18") { + addColumn(tableName: "keyword") { + column(name: "owner_detail", type: "varchar(255)") + } + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-20") { + addColumn(tableName: "smslib_fconnection") { + column(name: "manufacturer", type: "varchar(255)") + } + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-21") { + addColumn(tableName: "smslib_fconnection") { + column(name: "model", type: "varchar(255)") + } + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-22") { + addNotNullConstraint(columnDataType: "varchar(1600)", columnName: "TEXT", tableName: "FMESSAGE") + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-23") { + addPrimaryKey(columnNames: "connection_id, dispatch_id", constraintName: "smssync_dispaPK", tableName: "smssync_dispatch") + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-25") { + addForeignKeyConstraint(baseColumnNames: "autoforward_contacts_id", baseTableName: "autoforward_contact", constraintName: "FK8954F717D290A53C", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "autoforward", referencesUniqueColumn: "false") + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-26") { + addForeignKeyConstraint(baseColumnNames: "contact_id", baseTableName: "autoforward_contact", constraintName: "FK8954F7176256D8C2", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "contact", referencesUniqueColumn: "false") + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-27") { + addForeignKeyConstraint(baseColumnNames: "autoforward_groups_id", baseTableName: "autoforward_grup", constraintName: "FK3C39752FC133A1DB", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "autoforward", referencesUniqueColumn: "false") + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-28") { + addForeignKeyConstraint(baseColumnNames: "group_id", baseTableName: "autoforward_grup", constraintName: "FK3C39752F9083EA62", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "grup", referencesUniqueColumn: "false") + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-29") { + addForeignKeyConstraint(baseColumnNames: "autoforward_smart_groups_id", baseTableName: "autoforward_smart_group", constraintName: "FK4D8334002A1445A5", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "autoforward", referencesUniqueColumn: "false") + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-30") { + addForeignKeyConstraint(baseColumnNames: "smart_group_id", baseTableName: "autoforward_smart_group", constraintName: "FK4D8334003BEF92BF", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "smart_group", referencesUniqueColumn: "false") + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-31") { + addForeignKeyConstraint(baseColumnNames: "fconnection_messages_id", baseTableName: "fconnection_fmessage", constraintName: "FKD49CD6FC51C23BBF", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "fconnection", referencesUniqueColumn: "false") + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-32") { + addForeignKeyConstraint(baseColumnNames: "fmessage_id", baseTableName: "fconnection_fmessage", constraintName: "FKD49CD6FC92DDC012", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "fmessage", referencesUniqueColumn: "false") + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-33") { + addForeignKeyConstraint(baseColumnNames: "connection_id", baseTableName: "request_parameter", constraintName: "FKF047F9F95F834456", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "webconnection", referencesUniqueColumn: "false") + } + + changeSet(author: "geoffrey (generated)", id: "1355230052153-34") { + addForeignKeyConstraint(baseColumnNames: "group_id", baseTableName: "subscription", constraintName: "FK1456591D9083EA62", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "grup", referencesUniqueColumn: "false") + } + + //> POLL, ALIAS AND KEYWORD TRANSFORMATIONS + changeSet(author: "sitati", id:"1355230052153-35") { + grailsChange{ + change{ + // first set all existing keywords as top-level, and as first in the keyword list for the poll + sql.executeUpdate("UPDATE keyword SET is_top_level = true, keywords_idx = 0") + sql.eachRow("SELECT * FROM POLL") { poll -> + println "MIGRATIONS:::::::::: about to migrate poll: ${poll}" + // check if poll has keywords (if not, it has automatic sorting disabled, no need to act on aliases) + def pollKeywordCount = 0 + sql.eachRow("SELECT * FROM keyword WHERE activity_id = ${poll.ID}") { pollKeyword -> pollKeywordCount += 1 } + if(pollKeywordCount) { + def pollKeywordIndex = 1 // because top level keyword already set as zero + sql.eachRow("SELECT * FROM POLL_RESPONSE WHERE POLL_ID = ${poll.ID}") { pollResponse -> + println "MIGRATIONS:::::::::: for poll: ${poll}, migrating poll response:::: ${pollResponse}" + pollResponse.ALIASES?.split(',').each { aliasValue -> + println "MIGRATIONS:::::::::: for poll: ${poll}, migrating poll response ${pollResponse}: alias::: ${aliasValue}" + sql.execute("INSERT INTO keyword (activity_id, owner_detail, value, is_top_level, keywords_idx) values ($poll.ID, $pollResponse.KEY, ${aliasValue.trim().toUpperCase()}, false, $pollKeywordIndex)") + pollKeywordIndex += 1 + } + } + } + else { + println "Poll had no keywords, skipping alias migration" + } + // update ${contact_name} and ${contact_number} substitutions to ${recipient_name} and ${recipient_number} + if(poll.AUTOREPLY_TEXT?.contains('${contact_name}') || poll.AUTOREPLY_TEXT?.contains('${contact_number}')) { + def newAutoreplyText = poll.AUTOREPLY_TEXT.replace('${contact_name}', '${recipient_name}').replace('${contact_number}', '${recipient_number}').replace('"', '\\"') + sql.executeUpdate("UPDATE poll SET AUTOREPLY_TEXT = $newAutoreplyText WHERE poll.ID = ${poll.id}") + } + } + } + } + } + + //> AUTOREPLY TRANSFORMATIONS + changeSet(author: "sitati", id:"1355230052153-36") { + grailsChange{ + change{ + sql.eachRow("SELECT * FROM AUTOREPLY") { autoreply -> + // update ${contact_name} and ${contact_number} substitutions to ${recipient_name} and ${recipient_number} + if(autoreply.AUTOREPLY_TEXT?.contains('${contact_name}') || autoreply.AUTOREPLY_TEXT?.contains('${contact_number}')) { + def newAutoreplyText = autoreply.AUTOREPLY_TEXT.replace('${contact_name}', '${recipient_name}').replace('${contact_number}', '${recipient_number}').replace('"', '\\"') + sql.executeUpdate("UPDATE autoreply SET AUTOREPLY_TEXT = '"+newAutoreplyText + "' WHERE autoreply.ID = ${autoreply.id}") + } + } + } + } + } + + //> POLL RESPONSE ALIAS CLEANUP + changeSet(author: "geoffrey (generated)", id: "1355230052153-37") { + dropColumn(columnName: "ALIASES", tableName: "POLL_RESPONSE") + } + + //> INSTATING REQUIRED NOT_NULL CONSTRAINT ON KEYWORD.IS_TOP_LEVEL + changeSet(author: "geoffrey (generated)", id: "1355230052153-38") { + addNotNullConstraint(columnDataType: "boolean", columnName: "IS_TOP_LEVEL", tableName: "KEYWORD") + } + + //> CLICKATEL FCONNECTION TRANSFORMATIONS + changeSet(author: "sitati", id:"1355230052153-39") { + grailsChange{ + change{ + sql.executeUpdate("UPDATE clickatell_fconnection SET send_to_usa = false") + } + } + } +} diff --git a/plugins/frontlinesms-core/grails-app/migrations/changelog-3.0.groovy b/plugins/frontlinesms-core/grails-app/migrations/changelog-3.0.groovy new file mode 100644 index 000000000..64ea2f747 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/migrations/changelog-3.0.groovy @@ -0,0 +1,312 @@ +databaseChangeLog = { + + changeSet(author: "sitati (generated)", id: "1379593412207-1") { + createTable(tableName: "custom_activity") { + column(name: "id", type: "bigint") { + constraints(nullable: "false", primaryKey: "true", primaryKeyName: "custom_activiPK") + } + } + } + + changeSet(author: "sitati (generated)", id: "1379593412207-2") { + createTable(tableName: "message_detail") { + column(autoIncrement: "true", name: "id", type: "bigint") { + constraints(nullable: "false", primaryKey: "true", primaryKeyName: "message_detaiPK") + } + + column(name: "version", type: "bigint") { + constraints(nullable: "false") + } + + column(name: "message_id", type: "bigint") { + constraints(nullable: "false") + } + + column(name: "owner_id", type: "bigint") { + constraints(nullable: "false") + } + + column(name: "owner_type", type: "varchar(255)") { + constraints(nullable: "false") + } + + column(name: "value", type: "varchar(255)") { + constraints(nullable: "false") + } + } + } + + // Migrate old TextMessage.ownerDetail entries to new Message Detail domain + // can safely assume that all existing owners are ACTIVITY, not STEP, as custom activity not released yet + changeSet(author: "sitati", id:"1379593412207-3") { + grailsChange{ + change{ + sql.eachRow("SELECT * FROM FMESSAGE") { fmessage -> + if(fmessage.OWNER_DETAIL && fmessage.MESSAGE_OWNER_ID) { + sql.execute("INSERT INTO message_detail (version, message_id, owner_id, owner_type, value) VALUES (0, ${fmessage.ID}, ${fmessage.MESSAGE_OWNER_ID}, 'ACTIVITY', '${fmessage.OWNER_DETAIL}')") + } + } + } + } + } + + changeSet(author: "sitati (generated)", id: "1379593412207-4") { + createTable(tableName: "nexmo_fconnection") { + column(name: "id", type: "bigint") { + constraints(nullable: "false", primaryKey: "true", primaryKeyName: "nexmo_fconnecPK") + } + + column(name: "api_key", type: "varchar(255)") { + constraints(nullable: "false") + } + + column(name: "api_secret", type: "varchar(255)") { + constraints(nullable: "false") + } + + column(name: "from_number", type: "varchar(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "sitati (generated)", id: "1379593412207-5") { + createTable(tableName: "smpp_fconnection") { + column(name: "id", type: "bigint") { + constraints(nullable: "false", primaryKey: "true", primaryKeyName: "smpp_fconnectPK") + } + + column(name: "from_number", type: "varchar(255)") + + column(name: "smpp_password", type: "varchar(255)") + + column(name: "port", type: "varchar(255)") + + column(name: "receive", type: "boolean") + + column(name: "send", type: "boolean") + + column(name: "url", type: "varchar(255)") + + column(name: "username", type: "varchar(255)") + } + } + + changeSet(author: "sitati (generated)", id: "1379593412207-6") { + createTable(tableName: "step") { + column(autoIncrement: "true", name: "id", type: "bigint") { + constraints(nullable: "false", primaryKey: "true", primaryKeyName: "stepPK") + } + + column(name: "version", type: "bigint") { + constraints(nullable: "false") + } + + column(name: "activity_id", type: "bigint") { + constraints(nullable: "false") + } + + column(name: "class", type: "varchar(255)") { + constraints(nullable: "false") + } + + column(name: "steps_idx", type: "integer") + } + } + + changeSet(author: "sitati (generated)", id: "1379593412207-7") { + createTable(tableName: "step_property") { + column(autoIncrement: "true", name: "id", type: "bigint") { + constraints(nullable: "false", primaryKey: "true", primaryKeyName: "step_propertyPK") + } + + column(name: "version", type: "bigint") { + constraints(nullable: "false") + } + + column(name: "key", type: "varchar(255)") { + constraints(nullable: "false") + } + + column(name: "step_id", type: "bigint") { + constraints(nullable: "false") + } + + column(name: "value", type: "varchar(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "sitati (generated)", id: "1379593412207-8") { + addColumn(tableName: "dispatch") { + column(name: "fconnection_id", type: "bigint") + } + } + + changeSet(author: "sitati (generated)", id: "1379593412207-9") { + addColumn(tableName: "fconnection") { + column(name: "enabled", type: "boolean") + } + } + + changeSet(author: "sitati (generated)", id: "1379593412207-10") { + addColumn(tableName: "fconnection") { + column(name: "receive_enabled", type: "boolean") + } + } + + changeSet(author: "sitati (generated)", id: "1379593412207-11") { + addColumn(tableName: "fconnection") { + column(name: "send_enabled", type: "boolean") + } + } + + // Set default values for fconnection.send_enabled, fconnection.receive_enabled and fconnection.enabled + changeSet(author: "sitati", id:"1379593412207-12") { + grailsChange{ + change{ + // prior to this, existing fconnection implementations are 2-way, except clickatell + sql.executeUpdate("UPDATE FCONNECTION SET send_enabled = true, receive_enabled = true, enabled = true") + sql.executeUpdate("UPDATE FCONNECTION SET receive_enabled = false where ID in (select ID from CLICKATELL_FCONNECTION)") + } + } + } + + changeSet(author: "sitati (generated)", id: "1379593412207-13") { + addColumn(tableName: "search") { + column(name: "starred_only", type: "boolean") + } + grailsChange{ + change{ + sql.executeUpdate("UPDATE SEARCH SET starred_only = false") + } + } + } + + changeSet(author: "sitati (generated)", id: "1379593412207-14") { + addColumn(tableName: "smssync_fconnection") { + column(name: "timeout", type: "integer") + } + grailsChange{ + change{ + sql.executeUpdate("UPDATE smssync_fconnection SET timeout = 360") + } + } + } + + changeSet(author: "sitati (generated)", id: "1379593412207-15") { + addColumn(tableName: "system_notification") { + column(name: "topic", type: "varchar(255)") + } + } + + changeSet(author: "sitati (generated)", id: "1379593412207-16") { + addNotNullConstraint(columnDataType: "varchar(255)", columnName: "API_ID", tableName: "CLICKATELL_FCONNECTION") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-17") { + addNotNullConstraint(columnDataType: "varchar(255)", columnName: "CLICKATELL_PASSWORD", tableName: "CLICKATELL_FCONNECTION") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-18") { + addNotNullConstraint(columnDataType: "boolean", columnName: "SEND_TO_USA", tableName: "CLICKATELL_FCONNECTION") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-19") { + addNotNullConstraint(columnDataType: "varchar(255)", columnName: "USERNAME", tableName: "CLICKATELL_FCONNECTION") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-20") { + addNotNullConstraint(columnDataType: "varchar(255)", columnName: "KEY", tableName: "POLL_RESPONSE") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-21") { + dropForeignKeyConstraint(baseTableName: "POLL_RESPONSE_FMESSAGE", baseTableSchemaName: "PUBLIC", constraintName: "FK76CBE69F92DDC012") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-22") { + addForeignKeyConstraint(baseColumnNames: "message_id", baseTableName: "message_detail", constraintName: "FK21B74F893FBE872C", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "fmessage", referencesUniqueColumn: "false") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-23") { + addForeignKeyConstraint(baseColumnNames: "activity_id", baseTableName: "step", constraintName: "FK3606CC931DFDE3", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "custom_activity", referencesUniqueColumn: "false") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-24") { + addForeignKeyConstraint(baseColumnNames: "step_id", baseTableName: "step_property", constraintName: "FK9E9EDE8A35C3032", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "step", referencesUniqueColumn: "false") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-25") { + dropColumn(columnName: "OWNER_DETAIL", tableName: "FMESSAGE") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-26") { + dropColumn(columnName: "RECEIVE", tableName: "INTELLI_SMS_FCONNECTION") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-28") { + dropColumn(columnName: "SEND", tableName: "INTELLI_SMS_FCONNECTION") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-29") { + dropColumn(columnName: "RESPONSES_IDX", tableName: "POLL_RESPONSE") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-30") { + dropColumn(columnName: "RECEIVE", tableName: "SMSLIB_FCONNECTION") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-31") { + dropColumn(columnName: "SEND", tableName: "SMSLIB_FCONNECTION") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-32") { + dropColumn(columnName: "RECEIVE_ENABLED", tableName: "SMSSYNC_FCONNECTION") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-33") { + dropColumn(columnName: "SEND_ENABLED", tableName: "SMSSYNC_FCONNECTION") + } + + // Migrate poll_response_fmessage data into message_detail + changeSet(author: "sitati", id:"1379593412207-34") { + grailsChange{ + change{ + sql.eachRow("SELECT * FROM POLL_RESPONSE_FMESSAGE") { prf -> + def messageDetailValue + def pollId + sql.eachRow("SELECT * FROM POLL_RESPONSE where ID = ${prf.POLL_RESPONSE_MESSAGES_ID}"){ pollResponse -> + messageDetailValue = (pollResponse.key == 'unknown') ? 'unknown' : pollResponse.id + pollId = pollResponse.POLL_ID + } + sql.execute("INSERT INTO message_detail (version, message_id, owner_id, owner_type, value) VALUES (0, ${prf.FMESSAGE_ID}, ${pollId}, 'ACTIVITY', '${messageDetailValue}')") + } + } + } + } + + changeSet(author: "sitati (generated)", id: "1379593412207-35") { + dropTable(tableName: "POLL_RESPONSE_FMESSAGE") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-36") { + addNotNullConstraint(columnDataType: "boolean", columnName: "enabled", tableName: "fconnection") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-37") { + addNotNullConstraint(columnDataType: "boolean", columnName: "send_enabled", tableName: "fconnection") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-38") { + addNotNullConstraint(columnDataType: "boolean", columnName: "receive_enabled", tableName: "fconnection") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-39") { + addNotNullConstraint(columnDataType: "boolean", columnName: "starred_only", tableName: "search") + } + + changeSet(author: "sitati (generated)", id: "1379593412207-40") { + addNotNullConstraint(columnDataType: "integer", columnName: "timeout", tableName: "smssync_fconnection") + } +} diff --git a/plugins/frontlinesms-core/grails-app/migrations/changelog-3.1.groovy b/plugins/frontlinesms-core/grails-app/migrations/changelog-3.1.groovy new file mode 100644 index 000000000..d4178606d --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/migrations/changelog-3.1.groovy @@ -0,0 +1,30 @@ +databaseChangeLog = { + + changeSet(author: "geoffrey (generated)", id: "1387266424769-1") { + createTable(tableName: "frontlinesync_fconnection") { + column(name: "id", type: "bigint") { + constraints(nullable: "false", primaryKey: "true", primaryKeyName: "frontlinesyncPK") + } + + column(name: "last_connection_time", type: "timestamp") + + column(name: "secret", type: "varchar(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "geoffrey (generated)", id: "1387266424769-2") { + renameColumn(tableName: "fmessage", oldColumnName: "READ", newColumnName: "rd") + } + + changeSet(author: "geoffrey (generated)", id: "1387266424769-4") { + renameColumn(tableName: "system_notification", oldColumnName: "READ", newColumnName: "rd") + } + + changeSet(author: "geoffrey (generated)", id: "1387266424769-3") { + addColumn(tableName: "smssync_fconnection") { + column(name: "last_connection_time", type: "timestamp") + } + } +} diff --git a/plugins/frontlinesms-core/grails-app/migrations/changelog-v2.4.groovy b/plugins/frontlinesms-core/grails-app/migrations/changelog-v2.4.groovy new file mode 100644 index 000000000..6e338040d --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/migrations/changelog-v2.4.groovy @@ -0,0 +1,147 @@ +databaseChangeLog = { + + //Rename table SMSSYNC_DISPATCH to QUEUED_DISPATCH + changeSet(author: "vaneyck (by hand)", id: "rename-smssync-dispatch-table-1372420837873-1") { + renameTable(oldTableName: "SMSSYNC_DISPATCH", newTableName: "QUEUED_DISPATCH") + } + + changeSet(author: "vaneyck (generated)", id: "1387530956381-1") { + addColumn(tableName: "FCONNECTION_FMESSAGE") { + column(name: "TEXT_MESSAGE_ID", type: "BIGINT") + } + } + + // Copy all fconnection_fmessage[i].fmessage_id to fconnection_fmessage[i].text_message_id + changeSet(author: "vaneyck", id:"1387530956381-4") { + grailsChange{ + change{ + sql.executeUpdate("UPDATE FCONNECTION_FMESSAGE SET TEXT_MESSAGE_ID = FMESSAGE_ID") + } + } + } + + //Add CLASS property to fmessage table + changeSet(author: "vaneyck (generated)", id: "1400489400949-2") { + addColumn(tableName: "FMESSAGE") { + column(name: "CLASS", type: "VARCHAR(255)") { + constraints(nullable: "true") + } + } + } + + //Set all fmessage.class to frontlinesms2.TextMessage + changeSet(author: "vaneyck", id:"1387530956381-3") { + grailsChange{ + change{ + sql.executeUpdate("UPDATE FMESSAGE SET CLASS = 'frontlinesms2.TextMessage'") + } + } + } + + changeSet(author: "vaneyck (by hand)", id: "addNotNullConstraint-to-fmessage.class-1387530956381-3") { + addNotNullConstraint(columnDataType: "VARCHAR", columnName: "CLASS", tableName: "FMESSAGE") + } + + //Move FCONNECTION_FMESSAGE data to the FMESSAGE table CONNECTION_ID + changeSet(author: "vaneyck (generated)", id: "1400489400949-3") { + addColumn(tableName: "FMESSAGE") { + column(name: "CONNECTION_ID", type: "BIGINT") + } + grailsChange { + change { + sql.execute 'UPDATE FMESSAGE M SET CONNECTION_ID = (SELECT FCONNECTION_MESSAGES_ID FROM FCONNECTION_FMESSAGE FM WHERE FM.TEXT_MESSAGE_ID = M.ID)' + } + } + + } + + changeSet(author: "vaneyck (generated)", id: "1400489400949-4") { + addColumn(tableName: "FRONTLINESYNC_FCONNECTION") { + column(name: "CHECK_INTERVAL", type: "INT") { + constraints(nullable: "true") + } + } + } + + changeSet(author: "vaneyck (generated)", id: "1400489400949-5") { + addColumn(tableName: "FRONTLINESYNC_FCONNECTION") { + column(name: "CONFIG_SYNCED", type: "BOOLEAN", defaultValue: true) { + constraints(nullable: "false") + } + } + } + + changeSet(author: "vaneyck (generated)", id: "1400489400949-6") { + addColumn(tableName: "FRONTLINESYNC_FCONNECTION") { + column(name: "HAS_DISPATCHES", type: "BOOLEAN", defaultValue: true) { + constraints(nullable: "false") + } + } + } + + changeSet(author: "vaneyck (generated)", id: "1400489400949-7") { + addColumn(tableName: "FRONTLINESYNC_FCONNECTION") { + column(name: "MISSED_CALL_ENABLED", type: "BOOLEAN", defaultValue: true) { + constraints(nullable: "false") + } + } + } + + changeSet(author: "vaneyck (generated)", id: "1400489400949-8") { + addColumn(tableName: "SMSSYNC_FCONNECTION") { + column(name: "HAS_DISPATCHES", type: "BOOLEAN", defaultValue: true) { + constraints(nullable: "false") + } + } + } + + //Set default values for FRONTLINESYNC_FCONNECTION properties [CONFIG_SYNCED, MISSED_CALL_ENABLED, CHECK_INTERVAL] + changeSet(author: "vaneyck (by hand)", id: "set_default_values_for_smssync_and_frontlinesync_properties") { + grailsChange{ + change{ + sql.executeUpdate("UPDATE FRONTLINESYNC_FCONNECTION SET CONFIG_SYNCED = 1") + sql.executeUpdate("UPDATE FRONTLINESYNC_FCONNECTION SET MISSED_CALL_ENABLED = 1") + sql.executeUpdate("UPDATE FRONTLINESYNC_FCONNECTION SET CHECK_INTERVAL = 1") + } + } + } + + // FRONTLINESYNC_FCONNECTION.CHECK_INTERVAL should be nullable + changeSet(author: "vaneyck|sitati (by hand)", id: "added-check-interval-to-frontlinesync-table-1372420837874-2") { + addNotNullConstraint(columnDataType: "BIGINT", columnName: "CHECK_INTERVAL", tableName: "FRONTLINESYNC_FCONNECTION") + } + + //Set default values for HAS_DISPATCHES on FRONTLINESYNC_FCONNECTION and SMSSYNC_FCONNECTION + changeSet(author: "vaneyck (generated)", id: "1399636740836-3") { + grailsChange { + change { + sql.executeUpdate("UPDATE FRONTLINESYNC_FCONNECTION SET HAS_DISPATCHES = 1") + sql.executeUpdate("UPDATE SMSSYNC_FCONNECTION SET HAS_DISPATCHES = 1") + } + } + } + + changeSet(author: "vaneyck (generated)", id: "1400489400949-9") { + dropNotNullConstraint(columnDataType: "VARCHAR(1600)", columnName: "TEXT", tableName: "FMESSAGE") + } + + changeSet(author: "vaneyck (generated)", id: "1400489400949-10") { + modifyDataType(columnName: "TEXT", newDataType: "VARCHAR(511)", tableName: "SYSTEM_NOTIFICATION") + } + + changeSet(author: "vaneyck (generated)", id: "1400489400949-12") { + dropForeignKeyConstraint(baseTableName: "FCONNECTION_FMESSAGE", baseTableSchemaName: "PUBLIC", constraintName: "FKD49CD6FC51C23BBF") + } + + changeSet(author: "vaneyck (generated)", id: "1400489400949-13") { + dropForeignKeyConstraint(baseTableName: "FCONNECTION_FMESSAGE", baseTableSchemaName: "PUBLIC", constraintName: "FKD49CD6FC92DDC012") + } + + changeSet(author: "vaneyck (generated)", id: "1400489400949-17") { + dropColumn(columnName: "VERSION", tableName: "FCONNECTION") + } + + changeSet(author: "vaneyck (generated)", id: "1400489400949-18") { + dropTable(tableName: "FCONNECTION_FMESSAGE") + } +} diff --git a/plugins/frontlinesms-core/grails-app/migrations/changelog.groovy b/plugins/frontlinesms-core/grails-app/migrations/changelog.groovy index 664031784..60ce82197 100644 --- a/plugins/frontlinesms-core/grails-app/migrations/changelog.groovy +++ b/plugins/frontlinesms-core/grails-app/migrations/changelog.groovy @@ -20,5 +20,13 @@ databaseChangeLog = { include file: 'changelog-0.4.groovy' include file: 'changelog-1.0-rc2.groovy' + + include file: 'changelog-2.0.groovy' + + include file: 'changelog-3.0.groovy' + + include file: 'changelog-3.1.groovy' + + include file: 'changelog-v2.4.groovy' } diff --git a/plugins/frontlinesms-core/grails-app/routes/DispatchRoute.groovy b/plugins/frontlinesms-core/grails-app/routes/DispatchRoute.groovy index 4fa1bb544..53444f700 100644 --- a/plugins/frontlinesms-core/grails-app/routes/DispatchRoute.groovy +++ b/plugins/frontlinesms-core/grails-app/routes/DispatchRoute.groovy @@ -1,5 +1,7 @@ import org.apache.camel.builder.RouteBuilder +import frontlinesms2.camel.exception.NoRouteAvailableException + class DispatchRoute extends RouteBuilder { void configure() { onCompletion().onCompleteOnly() @@ -8,7 +10,12 @@ class DispatchRoute extends RouteBuilder { .beanRef('dispatchRouterService', 'handleFailed') from('seda:dispatches') + .onException(NoRouteAvailableException) + .beanRef('dispatchRouterService', 'handleNoRoutes') + .handled(false) + .end() .dynamicRouter(bean('dispatchRouterService', 'slip')) .routeId('dispatch-route') } } + diff --git a/plugins/frontlinesms-core/grails-app/routes/IncomingMissedCallRoute.groovy b/plugins/frontlinesms-core/grails-app/routes/IncomingMissedCallRoute.groovy new file mode 100644 index 000000000..caa73004e --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/routes/IncomingMissedCallRoute.groovy @@ -0,0 +1,9 @@ +import org.apache.camel.builder.RouteBuilder + +class IncomingMissedCallRoute extends RouteBuilder { + void configure() { + from('seda:incoming-missedcalls-to-store'). + beanRef('messageStorageService', 'process'). + routeId('missedcall-storage') + } +} diff --git a/plugins/frontlinesms-core/grails-app/routes/IncomingFmessageRoute.groovy b/plugins/frontlinesms-core/grails-app/routes/IncomingTextMessageRoute.groovy similarity index 85% rename from plugins/frontlinesms-core/grails-app/routes/IncomingFmessageRoute.groovy rename to plugins/frontlinesms-core/grails-app/routes/IncomingTextMessageRoute.groovy index 241c026b1..9a1be0a07 100644 --- a/plugins/frontlinesms-core/grails-app/routes/IncomingFmessageRoute.groovy +++ b/plugins/frontlinesms-core/grails-app/routes/IncomingTextMessageRoute.groovy @@ -1,6 +1,6 @@ import org.apache.camel.builder.RouteBuilder -class IncomingFmessageRoute extends RouteBuilder { +class IncomingTextMessageRoute extends RouteBuilder { void configure() { from('seda:incoming-fmessages-to-store'). beanRef('messageStorageService', 'process'). diff --git a/plugins/frontlinesms-core/grails-app/routes/SmslibTranslationRoute.groovy b/plugins/frontlinesms-core/grails-app/routes/SmslibTranslationRoute.groovy index 8bb691f24..eade0dc01 100644 --- a/plugins/frontlinesms-core/grails-app/routes/SmslibTranslationRoute.groovy +++ b/plugins/frontlinesms-core/grails-app/routes/SmslibTranslationRoute.groovy @@ -3,8 +3,9 @@ import org.apache.camel.builder.RouteBuilder class SmslibTranslationRoute extends RouteBuilder { void configure() { from('seda:raw-smslib') - .beanRef('smslibTranslationService', 'toFmessage') + .beanRef('smslibTranslationService', 'toTextMessage') .to('seda:incoming-fmessages-to-store') .routeId('smslib-translation') } } + diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/AnnouncementService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/AnnouncementService.groovy index b9ca90c9b..7e4e8379a 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/AnnouncementService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/AnnouncementService.groovy @@ -1,7 +1,5 @@ package frontlinesms2 -import frontlinesms2.* - class AnnouncementService { def messageSendService @@ -9,12 +7,10 @@ class AnnouncementService { announcement.name = params.name announcement.sentMessageText = params.messageText def m = messageSendService.createOutgoingMessage(params) - println "text ### ${m.text}" - println "inbound ### ${m.inbound}" - println "params ### ${params}" - messageSendService.send(m) announcement.addToMessages(m) - announcement.save(failOnError:true,flush:true) - return announcement + if(announcement.save(failOnError:true)) { + messageSendService.send(m) + } + announcement } } diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/ApiService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/ApiService.groovy new file mode 100644 index 000000000..b32abef0d --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/ApiService.groovy @@ -0,0 +1,7 @@ +package frontlinesms2 + +class ApiService { + def invokeApiProcess(entity, controller) { + entity.apiProcess(controller) + } +} diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/AppInfoService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/AppInfoService.groovy new file mode 100644 index 000000000..a0eff2ba1 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/AppInfoService.groovy @@ -0,0 +1,22 @@ +package frontlinesms2 + +class AppInfoService { + def grailsApplication + def providers = [:] + + synchronized void registerProvider(String key, Closure provider) { + if(providers.containsKey(key)) { + throw new RuntimeException("Provider already registered for key: $key") + } + providers[key] = provider + } + + def provide(AppInfoController controller, String key, data) { + def provider + synchronized(this) { + provider = providers[key] + } + return provider.call(grailsApplication, controller, data) + } +} + diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/AppSettingsService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/AppSettingsService.groovy index b9e64f6df..7a93683d9 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/AppSettingsService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/AppSettingsService.groovy @@ -1,6 +1,7 @@ package frontlinesms2 class AppSettingsService { + static final SP = { key, _default='' -> [System.properties[key], System.env[key.toUpperCase().replace('.', '_')], _default].find { it != null } } def grailsApplication private static final File PROPERTIES_FILE = new File(ResourceUtils.resourceDirectory, 'app-settings.properties') private def settings = [:] diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/AutoforwardService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/AutoforwardService.groovy index f67c50127..9198bcc6e 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/AutoforwardService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/AutoforwardService.groovy @@ -1,18 +1,23 @@ package frontlinesms2 -import frontlinesms2.* +import grails.events.Listener +import groovy.sql.Sql +// TODO fix indentation of this class class AutoforwardService { + def recipientLookupService + def messageSendService + def dataSource def saveInstance(Autoforward autoforward, params) { - println "##### Saving Autoforward in Service" + log.info "##### Saving Autoforward in Service" autoforward.name = params.name ?: autoforward.name autoforward.sentMessageText = params.messageText ?: autoforward.sentMessageText autoforward.keywords?.clear() editContacts(autoforward, params) - println "##Just about to save" + log.info "##Just about to save" autoforward.save(flush:true, failOnError:true) - println "##Just saved round 1" + log.info "##Just saved round 1" if(params.sorting == 'global'){ autoforward.addToKeywords(new Keyword(value:'')) }else if(params.sorting == 'enabled'){ @@ -22,49 +27,89 @@ class AutoforwardService { autoforward.addToKeywords(keyword) } } else { - println "##### AutoforwardService.saveInstance() # removing keywords" + log.info "##### AutoforwardService.saveInstance() # removing keywords" } - println "# 2 ######### Saving Round 2 # $autoforward.errors.allErrors" + log.info "# 2 ######### Saving Round 2 # $autoforward.errors.allErrors" autoforward.save(failOnError:true,flush:true) - println autoforward.contacts - println autoforward.groups - println autoforward.smartGroups + log.info autoforward.contacts + log.info autoforward.groups + log.info autoforward.smartGroups return autoforward } def editContacts(autoforward, params){ + def recipients = params.recipients try{ - if(params.addresses){ - def newContacts = [params.addresses].flatten().collect { return Contact.findByMobile(it)?:new Contact(mobile:it, name:'').save(failOnError:true) } - def oldContacts = autoforward.contacts?:[] - (oldContacts - newContacts?:[]).each { autoforward.removeFromContacts(it) } - (newContacts?:[] - oldContacts).each { autoforward.addToContacts(it) } - } + def oldContacts = autoforward.contacts?:[] + def oldGroups = autoforward.groups?:[] + def oldSmartGroups = autoforward.smartGroups?:[] def newGroups = [] def newSmartGroups = [] + def newContacts = [] - if(params.groups){ - ([params.groups].flatten() - null).each{ - if(it?.startsWith('group')){ - newGroups << Group.get(it.substring(it.indexOf('-')+1)) - } else if (it?.startsWith('smartgroup')) { - newSmartGroups << SmartGroup.get(it.substring(it.indexOf('-')+1)) - } - } + if (recipients) { + newContacts = recipientLookupService.getContacts(recipients) + newContacts += recipientLookupService.getManualAddresses(recipients).collect { return Contact.findByMobile(it)?:new Contact(mobile:it, name:'').save(failOnError:true) } + + recipientLookupService.getGroups(recipients).each { newGroups << it } + recipientLookupService.getSmartGroups(recipients).each { newSmartGroups << it } } - def oldGroups = autoforward.groups?:[] + (oldContacts - newContacts?:[]).each { autoforward.removeFromContacts(it) } + (newContacts?:[] - oldContacts).each { autoforward.addToContacts(it) } + (oldGroups - newGroups?:[]).each{ autoforward.removeFromGroups(it) } (newGroups?:[] - oldGroups).each{ autoforward.addToGroups(it) } - def oldSmartGroups = autoforward.smartGroups?:[] + (oldSmartGroups - newSmartGroups?:[]).each{ autoforward.removeFromSmartGroups(it) } (newSmartGroups?:[] - oldSmartGroups).each{ autoforward.addToSmartGroups(it) } } catch(Exception e) { - println "# 1 ######### $autoforward.errors.allErrors" - println "# Can't Change Contacts,Groups,SmartGroups # $e" + log.info "# 1 ######### $autoforward.errors.allErrors" + log.info "# Can't Change Contacts,Groups,SmartGroups # $e" } autoforward } -} \ No newline at end of file + + def doForward(autoforwardOrStep, message) { + def m + if(autoforwardOrStep instanceof Activity) { + m = messageSendService.createOutgoingMessage([contacts:autoforwardOrStep.contacts, groups:autoforwardOrStep.groups?:[] + autoforwardOrStep.smartGroups?:[], messageText:autoforwardOrStep.sentMessageText]) + autoforwardOrStep.addToMessages(m) + autoforwardOrStep.addToMessages(message) + } else { + m = messageSendService.createOutgoingMessage([addresses:autoforwardOrStep.recipients , messageText:autoforwardOrStep.sentMessageText]) + } + message.messageOwner.addToMessages(m) + message.messageOwner.save(failOnError:true) + m.setMessageDetail(autoforwardOrStep, message.id) + messageSendService.send(m) + + autoforwardOrStep.save(failOnError:true) + } + + @Listener(topic='beforeDelete', namespace='gorm') + def handleDeletedContact(Contact contact) { + // TODO document why this is using raw SQL + log.info "### Removing Contact $contact from Autoforward ##" + new Sql(dataSource).execute('DELETE FROM autoforward_contact WHERE contact_id=?', [contact.id]) + return true + } + + @Listener(topic='beforeDelete', namespace='gorm') + def handleDeletedGroup(Group group) { + // TODO document why this is using raw SQL + log.info "## Removing Group $group From Autoforward" + new Sql(dataSource).execute('DELETE FROM autoforward_grup WHERE group_id=?', [group.id]) + return true + } + + @Listener(topic='beforeDelete', namespace='gorm') + def handleDeletedSmartGroup(SmartGroup smartGroup) { + // TODO document why this is using raw SQL + log.info "## Removing SmartGroup $smartGroup From Autoforward" + new Sql(dataSource).execute('DELETE FROM autoforward_smart_group WHERE smart_group_id=?', [smartGroup.id]) + return true + } +} diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/AutoreplyService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/AutoreplyService.groovy index c4abe00c5..4a6a0c477 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/AutoreplyService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/AutoreplyService.groovy @@ -1,8 +1,7 @@ package frontlinesms2 -import frontlinesms2.* - class AutoreplyService { + def messageSendService def saveInstance(Autoreply autoreply, params) { autoreply.name = params.name ?: autoreply.name autoreply.autoreplyText = params.messageText ?: autoreply.autoreplyText @@ -18,9 +17,30 @@ class AutoreplyService { autoreply.addToKeywords(keyword) } } else { - println "##### AutoreplyService.saveInstance() # removing keywords" + log.info "##### AutoreplyService.saveInstance() # removing keywords" } autoreply.save(failOnError:true, flush:true) } + + def doReply(activityOrStep, message) { + def autoreplyText + if (activityOrStep instanceof Activity) { + autoreplyText = activityOrStep.autoreplyText + } + else if (activityOrStep instanceof Step) { + autoreplyText = activityOrStep.getPropertyValue('autoreplyText') + } + def params = [:] + params.addresses = message.src + params.messageText = autoreplyText + def outgoingMessage = messageSendService.createOutgoingMessage(params) + message.messageOwner.addToMessages(outgoingMessage) + message.messageOwner.save(failOnError:true) + outgoingMessage.setMessageDetail(activityOrStep, message.id) + outgoingMessage.save(failOnError:true) + + messageSendService.send(outgoingMessage) + activityOrStep.save() + } } diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/ContactImportService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/ContactImportService.groovy new file mode 100644 index 000000000..5e4662edd --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/ContactImportService.groovy @@ -0,0 +1,225 @@ +package frontlinesms2 + +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.io.StringWriter +import java.security.MessageDigest + +import au.com.bytecode.opencsv.CSVWriter +import au.com.bytecode.opencsv.CSVParser +import ezvcard.* + +class ContactImportService { + private final STANDARD_FIELDS = ['Name':'name', 'Mobile Number':'mobile', + 'Email':'email', 'Group(s)':'groups', 'Notes':'notes'] + + def systemNotificationService + def i18nUtilService + def grailsLinkGenerator + def failedContactList = [] + def sessionFactory + + def synchronized saveFailedContacts(failedContactInstance) { + failedContactList << failedContactInstance + } + + def getFailedContactsByKey(k) { + def failedContactInstance = failedContactList.find { it.key == k } + def fileContents = failedContactInstance?.fileContent?:'' + def systemNotificationTopic = "failed.contact.${failedContactInstance?.key}" + failedContactList.remove(failedContactInstance) + def failedImportNotification = SystemNotification.findByTopic(systemNotificationTopic) + failedImportNotification?.read = true + failedImportNotification?.save() + fileContents + } + + def importContactCsv(params, request) { + log.info "ContactImportService.importContactCsv) :: ENTRY" + def savedCount = 0 + def processedCount = 0 + def headers + def parser = new CSVParser() + def failedLines = [] + params.csv.eachLine { line -> + if(processedCount % 100 == 0) { + cleanUpGorm() + } + def tokens = parser.parseLine(line) + if(!headers) headers = tokens + else try { + if(headers.any { it.size() == 0 }) { + throw new RuntimeException("Empty headers in some contact import columns") + } + Contact c = new Contact() + def groups + def customFields = [] + headers.eachWithIndex { key, i -> + def value = tokens[i] + if(key in STANDARD_FIELDS && key != 'Group(s)') { + c."${STANDARD_FIELDS[key]}" = value + } else if(key == 'Group(s)') { + def groupNames = getGroupNames(value) + groups = getGroups(groupNames) + } else { + if (value.size() > 0 ){ + customFields << new CustomField(name:key, value:value) + } + } + } + // TODO not sure why this has to be done in a new session, but grails + // can't cope with failed saves if we don't do this + Contact.withNewSession { + c.save(failOnError:true) + if(groups) groups.each { c.addToGroup(it) } + if(customFields) customFields.each { c.addToCustomFields(it) } + c.save() + } + ++savedCount + } catch(Exception ex) { + log.info i18nUtilService.getMessage(code: 'import.contact.save.error'), ex + log.info "ContactImportService.importContactsCsv :: exception :: $ex" + failedLines << tokens + } + } + + def failedLineWriter = new StringWriter() + if(failedLines) { + def writer + try { + writer = new CSVWriter(failedLineWriter) + writer.writeNext(headers) + failedLines.each { writer.writeNext(it) } + } finally { try { writer.close() } catch(Exception ex) {} } + } + + if(savedCount > 0 && !failedLines) { + systemNotificationService.create(code:'import.contact.complete', args:[savedCount]) + } else { + def failedContactInstance = new FailedContact('csv', failedLineWriter.toString()) + saveFailedContacts(failedContactInstance) + def downloadLink = grailsLinkGenerator.link(controller:'import', action:'failedContacts', params:[format:'csv', key:failedContactInstance.key]) + def aTag = "${i18nUtilService.getMessage(code:'download.label')}" + systemNotificationService.create(code:'import.contact.failed.info', topic:"failed.contact.${failedContactInstance.key}", args:[savedCount, failedLines.size(), aTag]) + } + SystemNotification.findByTopic('import.status')?.delete() + } + + def importContactVcard(params, request) { + log.info "ContactImportService.importContactVcard() :: ENTRY" + def failedVcards = [] + def savedCount = 0 + def uploadFile = request.getFile('contactImportFile') + def processCard = { v -> + def mobile = v.telephoneNumbers? v.telephoneNumbers.first(): null + if(mobile) { + mobile = mobile.text?: mobile.uri?.number?: '' + mobile = mobile.replaceAll(/[^+\d]/, '') + } + def email = v.emails? v.emails.first().value: '' + try { + Contact.withNewSession { + new Contact(name:v.formattedName.value, mobile:mobile, email:email).save(failOnError:true) + } + ++savedCount + } catch(Exception ex) { + failedVcards << v + } + } + def parse = { format='', exceptionClass=null -> + try { + Ezvcard."parse${format.capitalize()}"(uploadFile.inputStream) + .all() + .eachWithIndex { it, index -> + processCard it + if (index % 100 == 0) { + cleanUpGorm() + } + } + } catch(Exception ex) { + if(exceptionClass && ex.class.isAssignableFrom(exceptionClass)) { + return false + } + throw ex + } + return true + } + if(!(parse('xml', org.xml.sax.SAXParseException) || + parse('json', com.fasterxml.jackson.core.JsonParseException) || + (parse('html') && parse()))) { + systemNotificationService.create(code:'import.contact.failed.invalid.vcf.file') + throw new RuntimeException('Failed to parse vcf.') + } + + if(savedCount > 0 && !failedVcards) { + systemNotificationService.create(code:'import.contact.complete', args:[savedCount]) + } else { + def failedContactInstance = new FailedContact('vcf', Ezvcard.write(failedVcards).go()) + saveFailedContacts(failedContactInstance) + def downloadLink = grailsLinkGenerator.link(controller:'import', action:'failedContacts', params:[format:'vcf', key:failedContactInstance.key]) + def aTag = "${i18nUtilService.getMessage(code:'download.label')}" + systemNotificationService.create(code:'import.contact.failed.info', topic:"failed.contact.${failedContactInstance.key}",args:[savedCount, failedVcards.size(), aTag]) + } + SystemNotification.findByTopic('import.status')?.delete() + } + + private def getMessageFolder(name) { + Folder.findByName(name)?: new Folder(name:name).save(failOnError:true) + } + + private saveMessagesIntoFolder(version, fm){ + getMessageFolder("messages from "+version).addToMessages(fm) + } + + private def getGroupNames(csvValue) { + Set csvGroups = [] + csvValue.split("\\\\").each { gName -> + def longName + gName.split("/").each { shortName -> + csvGroups << shortName + longName = longName? "$longName-$shortName": shortName + csvGroups << longName + } + } + return csvGroups - '' + } + + private def getGroups(groupNames) { + groupNames.collect { name -> + name = name.trim() + Group.findByName(name)?: new Group(name:name).save(failOnError:true) + } + } + + private def getFailedContactsFile() { + if(!params.jobId || params.jobId!=UUID.fromString(params.jobId).toString()) params.jobId = UUID.randomUUID().toString() + def f = new File(ResourceUtils.resourcePath, "import_contacts_${params.jobId}.csv") + f.deleteOnExit() + return f + } + + private def cleanUpGorm() { + def session = sessionFactory.currentSession + session.flush() + session.clear() + } +} + +class FailedContact { + String key + String fileType + String fileContent + + FailedContact(fileType, fileContent) { + def dataToHash = new Date().toString() + fileContent + this.key = generateMD5(dataToHash.toString()) + this.fileType = fileType + this.fileContent = fileContent + } + + def generateMD5(String s) { + MessageDigest digest = MessageDigest.getInstance("MD5") + digest.update(s.bytes); + new BigInteger(1, digest.digest()).toString(16).padLeft(32, '0') + } +} diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/ContactSearchService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/ContactSearchService.groovy index 3d053c64d..0fe8c9ab8 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/ContactSearchService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/ContactSearchService.groovy @@ -2,32 +2,57 @@ package frontlinesms2 class ContactSearchService { static transactional = true + def i18nUtilService def contactList(params) { + def contactsSection = params?.groupId? Group.get(params.groupId): params.smartGroupId? SmartGroup.get(params.smartGroupId): null [contactInstanceList: getContacts(params), - contactInstanceTotal: countContacts(params), - contactsSection: params?.groupId? Group.get(params.groupId): params.smartGroupId? SmartGroup.get(params.smartGroupId): null] + contactInstanceTotal: Contact.count(), + contactsSection: contactsSection, + contactsSectionContactTotal: countContacts(params)] } - + private def getContacts(params) { def searchString = getSearchString(params) if(params.groupId) { - GroupMembership.searchForContacts(asLong(params.groupId), searchString, params.sort, - params.max, - params.offset) + if (Group.get(params.groupId)) { + return GroupMembership.searchForContacts(asLong(params.groupId), searchString, params.sort, + params.max, + params.offset) + } + return [] } else if(params.smartGroupId) { - SmartGroup.getMembersByNameIlike(asLong(params.smartGroupId), searchString, [max:params.max, offset:params.offset]) - } else Contact.findAllByNameIlikeOrMobileIlike(searchString, searchString, params) + return SmartGroup.getMembersByNameIlike(asLong(params.smartGroupId), searchString, [max:params.max, offset:params.offset]) + } + if(params.exclude) { + return Contact.withCriteria { + not { + 'in' 'id', params.exclude + } + or { + ilike 'name', searchString + ilike 'mobile', searchString + } + if(params.max) maxResults(params.max) + if(params.order && params.sort) order params.sort, params.order + } + } else { + return Contact.findAllByNameIlikeOrMobileIlike(searchString, searchString, params) + } } private def countContacts(params) { def searchString = getSearchString(params) if(params.groupId) { - GroupMembership.countSearchForContacts(asLong(params.groupId), searchString) + if (Group.get(params.groupId)) { + return GroupMembership.countSearchForContacts(asLong(params.groupId), searchString) + } + return 0 } else if(params.smartGroupId) { - SmartGroup.countMembersByNameIlike(asLong(params.smartGroupId), searchString) - } else Contact.countByNameIlikeOrMobileIlike(searchString, searchString) + return SmartGroup.countMembersByNameIlike(asLong(params.smartGroupId), searchString) + } + return Contact.countByNameIlikeOrMobileIlike(searchString, searchString) } private def getSearchString(params) { diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/CustomActivityService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/CustomActivityService.groovy new file mode 100644 index 000000000..eef723a46 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/CustomActivityService.groovy @@ -0,0 +1,72 @@ +package frontlinesms2 + +import org.codehaus.groovy.grails.web.json.JSONArray + +class CustomActivityService { + def recipientLookupService + def saveInstance(customActivity, params) { + def steps = new JSONArray(params.jsonToSubmit) + customActivity.name = params.name + //TODO DRY the functionality of creating and editing keywords + customActivity.keywords?.clear() + + def storedSteps = customActivity.steps + // FIXME please fix formatting + def stepsToDelete = (storedSteps*.id?:[]) - steps*.stepId.collect { (it != "")?(it as Long):null } + + // FIXME please add spaces before -> + stepsToDelete.each { it-> + customActivity.removeFromSteps(Step.get(it)) + } + customActivity.save(failOnError:true, flush:true) + + // FIXME please add spaces before -> + steps.each { step-> + // FIXME please fix formatting + def stepToEdit = customActivity.steps.find { "${it.id}" == step.stepId } ?: Step.implementations.find {it.shortName == step.stepType}.newInstance(step) + stepToEdit.stepProperties?.clear() + if(stepToEdit.id) stepToEdit.save(failOnError:true, flush:true) + // FIXME please add spaces after commas and before -> + step.each { k,v-> + if(k == "recipients") { + stepToEdit.setRecipients(recipientLookupService.contactSearchResults([recipients:v]).values() as List) + } // FIXME please sort out linebreaks after braces + else if(!(k in ["stepType", "stepId"])) { + stepToEdit.setPropertyValue(k,v) + } + } + // FIXME please add braces around if/else statements + if(!stepToEdit.id) + customActivity.addToSteps(stepToEdit) + else + stepToEdit.save() + } + + // FIXME why are there multiple saves here? + customActivity.save(flush:true, failOnError:true) + + // FIXME please add spaces before braces + if(params.sorting == 'global'){ + // FIXME should not need to explivitly invoke `new Keyword()` when calling addToKeywords + customActivity.addToKeywords(new Keyword(value:'')) + // FIXME please add spaces before braces + } else if(params.sorting == 'enabled'){ + def keywordRawValues = params.keywords?.toUpperCase().replaceAll(/\s/, "").split(",") + // FIXME please use .each() instead of for() + for(keywordValue in keywordRawValues) { + def keyword = new Keyword(value: keywordValue.trim().toUpperCase()) + customActivity.addToKeywords(keyword) + } + } + + // FIXME why are there multiple saves here? + customActivity.save(failOnError:true, flush:true) + } + + def triggerSteps(c, message) { + c.steps.each { + it.process(message) + } + } +} + diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/DataUploadService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/DataUploadService.groovy new file mode 100644 index 000000000..c46e53c42 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/DataUploadService.groovy @@ -0,0 +1,15 @@ +package frontlinesms2 + +import groovyx.net.http.HTTPBuilder + +class DataUploadService { + def upload(String url, Map dataToSend) { + def http = new HTTPBuilder(url) + boolean success = false + http.post(body: dataToSend, requestContentType:URLENC) { resp -> + success = resp.statusLine.statusCode == 200 + } + success + } +} + diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/DeviceDetectionService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/DeviceDetectionService.groovy index dbb9e9682..39e7661dd 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/DeviceDetectionService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/DeviceDetectionService.groovy @@ -15,12 +15,12 @@ class DeviceDetectionService { if(Environment.current != Environment.TEST) { def disableDetect = System.properties['serial.detect.disable'] - println "DeviceDetectionService.init() :: disableDetect? $disableDetect" + log.info "DeviceDetectionService.init() :: disableDetect? $disableDetect" if(!disableDetect || !Boolean.parseBoolean(disableDetect)) { - println 'DeviceDetectionService.init() :: detection enabled. Starting...' + log.info 'DeviceDetectionService.init() :: detection enabled. Starting...' detect() - } else println 'DeviceDetectionService.init() :: detection disabled.' - } else println 'DeviceDetectionService.init() :: detection disabled as grails environment is test.' + } else log.info 'DeviceDetectionService.init() :: detection disabled.' + } else log.info 'DeviceDetectionService.init() :: detection disabled as grails environment is test.' } def detect() { @@ -36,14 +36,14 @@ class DeviceDetectionService { } def stopFor(String port) { - println "DeviceDetectionService.stopFor($port)..." + log.info "DeviceDetectionService.stopFor($port)..." def detectorThread detector.detectors.each { - println "Checking $it.portName..." + log.info "Checking $it.portName..." if(it.portName == port) { - println "Port matched." + log.info "Port matched." detectorThread = it - } else println "Not the right port." + } else log.info "Not the right port." } if(detectorThread && detectorThread!=Thread.currentThread()) { detectorThread.interrupt() @@ -67,3 +67,4 @@ class DeviceDetectionService { return (detectorThread != null && threadState != Thread.State.TERMINATED) } } + diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/DeviceDetectorListenerService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/DeviceDetectorListenerService.groovy index 19d20f725..df51fcb5e 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/DeviceDetectorListenerService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/DeviceDetectorListenerService.groovy @@ -7,6 +7,7 @@ import serial.NoSuchPortException class DeviceDetectorListenerService implements ATDeviceDetectorListener { def fconnectionService def i18nUtilService + def appSettingsService /** * Handles completion of detection of a device on a database port. @@ -14,73 +15,83 @@ class DeviceDetectorListenerService implements ATDeviceDetectorListener { * connections to the same device. */ synchronized void handleDetectionCompleted(ATDeviceDetector detector) { - def log = { println "# $it" } - println "#####################################################" - log "deviceDetectionService.handleDetectionCompleted()" - log "port: [$detector.portName]" - log "manufacturer: [$detector.manufacturer]" - log "model: [$detector.model]" - log "imsi: [$detector.imsi]" - log "serial: [$detector.serial]" - log "SMS send supported: $detector.smsSendSupported" - log "SMS receive supported: $detector.smsReceiveSupported" + def logWithPrefix = { log.info "# $it" } + log.info "#####################################################" + logWithPrefix "deviceDetectionService.handleDetectionCompleted()" + logWithPrefix "port: [$detector.portName]" + logWithPrefix "manufacturer: [$detector.manufacturer]" + logWithPrefix "model: [$detector.model]" + logWithPrefix "imsi: [$detector.imsi]" + logWithPrefix "serial: [$detector.serial]" + logWithPrefix "SMS send supported: $detector.smsSendSupported" + logWithPrefix "SMS receive supported: $detector.smsReceiveSupported" if(!(detector.smsSendSupported || detector.smsReceiveSupported)) { - log "No point connecting if no SMS functionality is supported." + logWithPrefix "No point connecting if no SMS functionality is supported." return } - log "Available connections in database:" + logWithPrefix "Available connections in database:" SmslibFconnection.findAll().each { c -> - log " $c.id\t$c.port\t$c.serial\t$c.imsi" + logWithPrefix " $c.id\t$c.port\t$c.serial\t$c.imsi" } def matchingModemAndSim = SmslibFconnection.findAllBySerialAndImsi(detector.serial, detector.imsi) - log "Matching modem and SIM in database:" + logWithPrefix "Matching modem and SIM in database:" matchingModemAndSim.each { c -> - log " $c.id\t$c.port\t$c.serial\t$c.imsi" + logWithPrefix " $c.id\t$c.port\t$c.serial\t$c.imsi" } if(matchingModemAndSim.any { it.status == ConnectionStatus.CONNECTED || (it.port != detector.portName && isPortVisible(it.port)) }) { - log "There was a created route already on this device." + logWithPrefix "There was a created route already on this device." return } def connectionToStart def exactMatch = matchingModemAndSim.find { it.port == detector.portName } - if(exactMatch && exactMatch.status != ConnectionStatus.CONNECTED) { - log "Found exact match: $exactMatch" + if(exactMatch && !(exactMatch.status in [ConnectionStatus.CONNECTED, ConnectionStatus.DISABLED])) { + logWithPrefix "Found exact match: $exactMatch" connectionToStart = exactMatch } else { def c = SmslibFconnection.findForDetector(detector).list() if(c) { - log "Found for detector: $c" + logWithPrefix "Found for detector: $c" c = c[0] def dirty = !(c.serial && c.imsi) if(!c.serial) c.setSerial(detector.serial) if(!c.imsi) c.setImsi(detector.imsi) if(dirty) c.save() - connectionToStart = c + if(c.enabled) connectionToStart = c } else { def name = i18nUtilService.getMessage(code:'connection.name.autoconfigured', args:[ detector.manufacturer, detector.model, detector.portName]) - connectionToStart = new SmslibFconnection(name:name, port:detector.portName, baud:detector.maxBaudRate, + connectionToStart = new SmslibFconnection(name:name, + manufacturer:detector.manufacturer, model:detector.model, + port:detector.portName, baud:detector.maxBaudRate, serial:detector.serial, imsi:detector.imsi) .save(flush:true, failOnError:true) - log "Created new SmslibFconnection: $name" + logWithPrefix "Created new SmslibFconnection: $name" + addConnectionToRoutingRules(connectionToStart) } } if(connectionToStart) { - log "Starting connection $connectionToStart with imsi=$connectionToStart.imsi;serial=$connectionToStart.serial" + logWithPrefix "Starting connection $connectionToStart with imsi=$connectionToStart.imsi;serial=$connectionToStart.serial" fconnectionService.createRoutes(connectionToStart) } - log "After connection, smslibfconnections:" + logWithPrefix "After connection, smslibfconnections:" SmslibFconnection.withNewSession { SmslibFconnection.findAll().each { c -> - log " $c.id\t$c.port\t$c.serial\t$c.imsi" + logWithPrefix " $c.id\t$c.port\t$c.serial\t$c.imsi" } } } + private addConnectionToRoutingRules(connection) { + def connectionUseSetting = appSettingsService['routing.use'] + appSettingsService['routing.use'] = connectionUseSetting? + "$connectionUseSetting,fconnection-$connection.id": + "fconnection-$connection.id" + } + private boolean isPortVisible(String portName) { try { return CommPortIdentifier.getPortIdentifier(portName) != null diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/DispatchRouterService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/DispatchRouterService.groovy index adb3840f1..53414368b 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/DispatchRouterService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/DispatchRouterService.groovy @@ -3,12 +3,16 @@ package frontlinesms2 import org.apache.camel.Exchange import org.apache.camel.Header +import frontlinesms2.camel.exception.NoRouteAvailableException + /** This is a Dynamic Router */ class DispatchRouterService { + static final String RULE_PREFIX = "fconnection-" + def appSettingsService def camelContext + def systemNotificationService int counter = -1 - /** * Slip should return the list of ______ to forward to, or null if * we've done with it. @@ -16,36 +20,64 @@ class DispatchRouterService { def slip(Exchange exchange, @Header(Exchange.SLIP_ENDPOINT) String previous, @Header('requested-fconnection-id') String requestedFconnectionId) { - def log = { println "DispatchRouterService.slip() : $it" } - log "ENTRY" - log "Routing exchange $exchange with previous endpoint $previous and target fconnection $requestedFconnectionId" - log "x.in=$exchange?.in" - log "x.in.headers=$exchange?.in?.headers" - + def logWithPrefix = { log.info "slip() : $it" } + logWithPrefix "ENTRY" + logWithPrefix "Routing exchange $exchange with previous endpoint $previous and target fconnection $requestedFconnectionId" + logWithPrefix "x.in=$exchange?.in" + logWithPrefix "x.in.headers=$exchange?.in?.headers" + if(previous) { // We only want to pass this message to a single endpoint, so if there // is a previous one set, we should exit the slip. - log "Exchange has previous endpoint from this slip. Returning null." + logWithPrefix "Exchange has previous endpoint from this slip. Returning null." return null } else if(requestedFconnectionId) { - log "Target is set, so forwarding exchange to fconnection $requestedFconnectionId" + logWithPrefix "Target is set, so forwarding exchange to fconnection $requestedFconnectionId" return "seda:out-$requestedFconnectionId" } else { - def routeId = getDispatchRouteId() + def routeId + logWithPrefix "appSettingsService.['routing.use'] is ${appSettingsService.get('routing.use')}" + + if(appSettingsService.get('routing.use')) { + def fconnectionRoutingList = appSettingsService.get('routing.use').split(/\s*,\s*/) + fconnectionRoutingList = fconnectionRoutingList.collect { route -> + route.startsWith(RULE_PREFIX)? route.substring(RULE_PREFIX.size()): route + } + logWithPrefix "fconnectionRoutingList::: $fconnectionRoutingList" + for(route in fconnectionRoutingList) { + if(route == 'uselastreceiver') { + def fconnection = getLastReceiverConnection(exchange) + route = fconnection?.id + routeId = fconnection ? getCamelRouteId(fconnection) : null + } else { + routeId = getCamelRouteId(Fconnection.get(route)) + } + logWithPrefix "Route Id selected: $routeId" + if(routeId) { + if(exchange.in.body instanceof Dispatch) { + def dispatch = waitForDispatchToBePersisted(exchange.in.body.id) + logWithPrefix " dispatch.fconnectionId:::: $route" + dispatch.fconnectionId = route as Long + dispatch.save(failOnError:true, flush:true) + } + break + } + } + } + if(routeId) { - log "Sending with route: $routeId" + logWithPrefix "Sending with route: $routeId" def fconnectionId = (routeId =~ /.*-(\d+)$/)[0][1] def queueName = "seda:out-$fconnectionId" - log "Routing to $queueName" + logWithPrefix "Routing to $queueName" return queueName - } else { - // TODO may want to queue for retry here, after incrementing retry-count header - throw new RuntimeException("No outbound route available for dispatch.") - } + } else { logWithPrefix "## Not sending message at all ##" } + + throw new NoRouteAvailableException() } } - def getDispatchRouteId() { + def getRouteIdByRoundRobin() { def allOutRoutes = camelContext.routes.findAll { it.id.startsWith('out-') } if(allOutRoutes.size > 0) { // check for internet routes and prioritise them over modems @@ -53,24 +85,35 @@ class DispatchRouterService { if(!filteredRouteList) filteredRouteList = allOutRoutes.findAll { it.id.contains('-modem-') } if(!filteredRouteList) filteredRouteList = allOutRoutes - println "DispatchRouterService.getDispatchConnectionId() : Routes available: ${filteredRouteList*.id}" - println "DispatchRouterService.getDispatchConnectionId() : Counter has counted up to $counter" + log.info "getRouteIdByRoundRobin() : Routes available: ${filteredRouteList*.id}" + log.info "getRouteIdByRoundRobin() : Counter has counted up to $counter" return filteredRouteList[++counter % filteredRouteList.size]?.id } } def handleCompleted(Exchange x) { - println "DispatchRouterService.handleCompleted() : ENTRY" - updateDispatch(x, DispatchStatus.SENT) - println "DispatchRouterService.handleCompleted() : EXIT" + log.info "handleCompleted() : ENTRY" + def connection = Fconnection.get(x.in.getHeader('fconnection-id')) + connection?.updateDispatch(x) + log.info "handleCompleted() : EXIT" } def handleFailed(Exchange x) { - println "DispatchRouterService.handleFailed() : ENTRY" + log.info "handleFailed() : ENTRY" + log.info "handleFailed() : exchange $x" + log.info "handleFailed() : exchange.in.body ${x.in.body}" updateDispatch(x, DispatchStatus.FAILED) - println "DispatchRouterService.handleFailed() : EXIT" + log.info "handleFailed() : EXIT" } - + + def handleNoRoutes(Exchange x) { + log.info "handleNoRoutes() : NoRouteAvailableException handling..." + systemNotificationService.create(code:"routing.notification.no-available-route") + x.out.body = x.in.body + x.out.headers = x.in.headers + log.info "handleNoRoutes() : EXIT" + } + private Dispatch updateDispatch(Exchange x, s) { def id = x.in.getHeader('frontlinesms.dispatch.id') Dispatch d @@ -81,7 +124,7 @@ class DispatchRouterService { d = Dispatch.get(id) } - println "DispatchRouterService.updateDispatch() : dispatch=$d" + log.info "updateDispatch() : dispatch=$d" if(d) { d.status = s @@ -94,4 +137,43 @@ class DispatchRouterService { } } else log.info("No dispatch found for id: $id") } + + private getLastReceiverConnection(exchange) { + def logWithPrefix = { log.info "slip() : $it" } + logWithPrefix "Dispatch is ${exchange.in.getBody()}" + def d = exchange.in.getBody() + logWithPrefix "dispatch to send # $d ### d.dst # $d?.dst" + def latestReceivedMessage = TextMessage.findBySrc(d.dst, [sort: 'dateCreated', order:'desc']) + logWithPrefix "## latestReceivedMessage ## is $latestReceivedMessage" + latestReceivedMessage?.receivedOn + } + + private getCamelRouteId(connection) { + if(!connection) return null + log.info "## Sending message with Connection with $connection ##" + def allOutRoutes = camelContext.routes.findAll { it.id.startsWith('out-') } + def routeToTake = allOutRoutes.find{ it.id.endsWith("-${connection.id}") } + log.info "Chosen Route ## $routeToTake" + routeToTake? routeToTake.id: null + } + + private waitForDispatchToBePersisted(dispatchId, attemptCount=0) { + log.info "Checking for Dispatch $dispatchId in db..." + def d = Dispatch.get(dispatchId) + if (d) { + log.info "Dispatch $dispatchId successfully retrieved from db." + return d + } + else if(attemptCount < 10){ + def timeToSleep = 50 + (50 * attemptCount) + log.info "Dispatch not found in database. Waiting for $timeToSleep ms before checking again for dispatch $dispatchId in db" + sleep(timeToSleep) + return waitForDispatchToBePersisted(dispatchId, attemptCount+1) + } + else { + log.info "Dispatch not found in db after 10 attempts, throwing exception" + throw new NullPointerException("Dispatch not found in database") + } + } } + diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/EmailTranslationService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/EmailTranslationService.groovy index 7fb60c708..d676150a2 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/EmailTranslationService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/EmailTranslationService.groovy @@ -10,18 +10,18 @@ class EmailTranslationService implements Processor { static transactional = false void process(Exchange exchange) { - println("exchange ${exchange}") + log.info("exchange ${exchange}") def i = exchange.in - println("in: ${i}") - Fmessage message = new Fmessage(inbound:true) + log.info("in: ${i}") + TextMessage message = new TextMessage(inbound:true) message.src = EMAIL_PROTOCOL_PREFIX + i.getHeader('From') - println("src: ${message.src}") + log.info("src: ${message.src}") // message.dst = EMAIL_PROTOCOL_PREFIX + i.getHeader('To') -// println("dst: ${message.dst}") +// log.info("dst: ${message.dst}") def emailBody = i.body def emailSubject = i.getHeader('Subject') - println("emailBody: ${emailBody}") - println("emailSubject: ${emailSubject}") + log.info("emailBody: ${emailBody}") + log.info("emailSubject: ${emailSubject}") message.text = emailSubject if(emailBody != null) { message.text = message.text ? "${message.text}\n${underline(emailSubject)}\n\n${emailBody}" : emailBody diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/ExpressionProcessorService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/ExpressionProcessorService.groovy index f94310fcf..2374fc2bf 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/ExpressionProcessorService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/ExpressionProcessorService.groovy @@ -10,6 +10,7 @@ class ExpressionProcessorService { 'recipient_name' : ['quickMessage', 'announcement', 'poll', 'autoreply', 'subscription', 'autoforward'], 'sender_number' : ['autoforward'], 'sender_name' : ['autoforward'], + 'keyword' : ['autoforward'], 'message_text' : ['poll', 'autoreply', 'subscription','autoforward'], 'message_text_with_keyword' : ['quickMessage','poll', 'autoreply', 'subscription','autoforward']] @@ -17,20 +18,11 @@ class ExpressionProcessorService { fields.findAll { controllerName in it.value }.keySet() } - String replaceExpressions(Dispatch dispatch) { - def messageBody = dispatch.message.text - matches = messageBody.findAll(regex) - matches.each { - messageBody = messageBody.replaceFirst(regex, getReplacement(it, dispatch)) - } - messageBody - } - String process(Dispatch dispatch) { def messageBody = dispatch.message.text def matches = getExpressions(messageBody) - matches.each { - messageBody = messageBody.replaceFirst(regex, getReplacement(it, dispatch)) + matches.unique().each { match -> + messageBody = messageBody.replace(match, getReplacement(match, dispatch)) } messageBody } @@ -48,26 +40,44 @@ class ExpressionProcessorService { } private getReplacement(expression, dispatch) { - def incomingMessage = Fmessage.get(dispatch.message.ownerDetail) - if (expression == "\${message_text}"){ - def keyword = incomingMessage.messageOwner?.keywords?.find{ incomingMessage.text.toUpperCase().startsWith(it.value) }?.value - def text = incomingMessage.text - if (keyword?.size() && text.toUpperCase().startsWith(keyword.toUpperCase())) { - text = text.substring(keyword.size()).trim() + try { + // TODO could replace this manual mapping wth...a Map! e.g. [sender_number:{incomingMessage.src}] + if(!dispatch.message.isAttached()) { + dispatch.message.attach() + } + def ownerD = dispatch.message.ownerDetail + log.info "### Owner Detail for ${dispatch} ## ${ownerD}" + def incomingMessage = TextMessage.get(ownerD) + log.info "### Triggering incoming message # ${incomingMessage} # ${incomingMessage?.text}" + def getKeyword = { incomingMessage.messageOwner?.keywords?.find { incomingMessage.text.toUpperCase().startsWith(it.value) }?.value } + if (expression == '${message_text}') { + def keyword = getKeyword() + def text = incomingMessage.text + if (keyword?.size() && text.toUpperCase().startsWith(keyword.toUpperCase())) { + text = text.substring(keyword.size()).trim() + } + return text + } + if (expression == '${message_text_with_keyword}') + return incomingMessage.text + if (expression == '${sender_number}') + return incomingMessage.src + if (expression == '${sender_name}') + return incomingMessage.inboundContactName?: incomingMessage.src + if (expression == '${recipient_number}') + return dispatch.dst + if (expression == '${recipient_name}') { + def recipientName = Contact.findByMobile(dispatch.dst)?.name + return recipientName ?: dispatch.dst } - return text + if (expression == '${keyword}') + return getKeyword() + return expression + } catch (Exception e) { + log.info "Exception when processing substitution" + log.info e + return expression } - if (expression == "\${message_text_with_keyword}") - return incomingMessage.text - if (expression == "\${sender_number}") - return incomingMessage.src - if (expression == "\${sender_name}") - return Contact.findByMobileLike(incomingMessage.src)? Contact.findByMobileLike(incomingMessage.src).name : incomingMessage.src - if (expression == "\${recipient_number}") - return dispatch.dst - if (expression == "\${recipient_name}") - return Contact.findByMobileLike(dispatch.dst)? Contact.findByMobileLike(dispatch.dst).name : dispatch.dst - return "" } } diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/FailPendingMessagesService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/FailPendingMessagesService.groovy index e35ca0684..bf5693034 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/FailPendingMessagesService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/FailPendingMessagesService.groovy @@ -3,13 +3,17 @@ package frontlinesms2 class FailPendingMessagesService { def init() { // N.B. This should ONLY EVER be called in the bootstrap, and therefore probably shouldn't be a service + // FIXME i18n and should use SystemNotificationService def pendingDispatchList = Dispatch.findAllByStatus(DispatchStatus.PENDING) if (pendingDispatchList) { pendingDispatchList.each() { it.status = DispatchStatus.FAILED it.save() } - new SystemNotification(text:"${pendingDispatchList.size()} pending message(s) failed. Go to pending messages section to view.").save(failOnError:true) + def text = "${pendingDispatchList.size()} pending message(s) failed. Go to pending messages section to view." + def sn = SystemNotification.findOrCreateByText(text) + sn.read = false + sn.save(failOnError:true) } } diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/FconnectionService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/FconnectionService.groovy index a7c6bc0ce..8764bedec 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/FconnectionService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/FconnectionService.groovy @@ -9,9 +9,15 @@ class FconnectionService { def camelContext def deviceDetectionService def i18nUtilService - + def smssyncService + def logService + def systemNotificationService + def connectingIds = [].asSynchronized() + def messageSource + def createRoutes(Fconnection c) { - println "FconnectionService.createRoutes() :: ENTRY :: $c" + log.info "FconnectionService.createRoutes() :: ENTRY :: $c" + assert c.enabled if(c instanceof SmslibFconnection) { deviceDetectionService.stopFor(c.port) // work-around for CORE-736 - NoSuchPortException can be thrown @@ -21,83 +27,116 @@ class FconnectionService { CommPortIdentifier.getPortIdentifiers() } } - println "creating route for fconnection $c" + log.info "creating route for fconnection $c" try { + connectingIds << c.id def routes = c.routeDefinitions camelContext.addRouteDefinitions(routes) - createSystemNotification('connection.route.successNotification', [c?.name?: c?.id]) - LogEntry.log("Created routes: ${routes*.id}") + systemNotificationService.create(code:'connection.route.successNotification', args:[c?.name?: c?.id], kwargs:[connection:c]) + logService.handleRouteCreated(c) } catch(FailedToCreateProducerException ex) { logFail(c, ex.cause) } catch(Exception ex) { logFail(c, ex) destroyRoutes(c.id as long) + } finally { + connectingIds -= c.id } - println "FconnectionService.createRoutes() :: EXIT :: $c" + log.info "FconnectionService.createRoutes() :: EXIT :: $c" } - + def destroyRoutes(Fconnection c) { destroyRoutes(c.id as long) - createSystemNotification('connection.route.destroyNotification', [c?.name?: c?.id]) + systemNotificationService.create(code:'connection.route.disableNotification', args:[c?.name?: c?.id], kwargs:[connection:c]) } - + def destroyRoutes(long id) { - println "fconnectionService.destroyRoutes : ENTRY" - println "fconnectionService.destroyRoutes : id=$id" + log.info "fconnectionService.destroyRoutes : ENTRY" + log.info "fconnectionService.destroyRoutes : id=$id" camelContext.routes.findAll { it.id ==~ /.*-$id$/ }.each { try { - println "fconnectionService.destroyRoutes : route-id=$it.id" - println "fconnectionService.destroyRoutes : stopping route $it.id..." + log.info "fconnectionService.destroyRoutes : route-id=$it.id" + log.info "fconnectionService.destroyRoutes : stopping route $it.id..." camelContext.stopRoute(it.id) - println "fconnectionService.destroyRoutes : $it.id stopped. removing..." + log.info "fconnectionService.destroyRoutes : $it.id stopped. removing..." camelContext.removeRoute(it.id) - println "fconnectionService.destroyRoutes : $it.id removed." + log.info "fconnectionService.destroyRoutes : $it.id removed." } catch(Exception ex) { - println "fconnectionService.destroyRoutes : Exception thrown while destroying $it.id: $ex" + log.info "fconnectionService.destroyRoutes : Exception thrown while destroying $it.id: $ex" ex.printStackTrace() } } - println "fconnectionService.destroyRoutes : EXIT" + // TODO fire event to announce that route was destroyed + log.info "fconnectionService.destroyRoutes : EXIT" } - + def getConnectionStatus(Fconnection c) { - if (c instanceof SmslibFconnection) - return camelContext.routes.any { it.id ==~ /.*-$c.id$/ } ? ConnectionStatus.CONNECTED : deviceDetectionService.isConnecting((c as SmslibFconnection).port) ? ConnectionStatus.CONNECTING : ConnectionStatus.NOT_CONNECTED - else - return camelContext.routes.any { it.id ==~ /.*-$c.id$/ } ? ConnectionStatus.CONNECTED : ConnectionStatus.NOT_CONNECTED + if(!c.enabled) return ConnectionStatus.DISABLED + if(c.id in connectingIds) { + return ConnectionStatus.CONNECTING + } + if(camelContext.routes.any { it.id ==~ /in-$c.id$|out.*-$c.id$/ }) { + return ConnectionStatus.CONNECTED + } + if(c.hasProperty('customStatus')) { + return c.customStatus + } + return ConnectionStatus.FAILED } - + // TODO rename 'handleNotConnectedException' def handleDisconnection(Exchange ex) { try { - println "fconnectionService.handleDisconnection() : ENTRY" + log.info "fconnectionService.handleDisconnection() : ENTRY" def caughtException = ex.getProperty(Exchange.EXCEPTION_CAUGHT) - println "FconnectionService.handleDisconnection() : ex.fromRouteId: $ex.fromRouteId" - println "FconnectionService.handleDisconnection() : EXCEPTION_CAUGHT: $caughtException" + log.info "FconnectionService.handleDisconnection() : ex.fromRouteId: $ex.fromRouteId" + log.info "FconnectionService.handleDisconnection() : EXCEPTION_CAUGHT: $caughtException" log.warn("Caught exception for route: $ex.fromRouteId", caughtException) def routeId = (ex.fromRouteId =~ /(?:(?:in)|(?:out))-(?:[a-z]+-)?(\d+)/)[0][1] - println "FconnectionService.handleDisconnection() : Looking to stop route: $routeId" - createSystemNotification('connection.route.exception', [routeId], caughtException) + log.info "FconnectionService.handleDisconnection() : Looking to stop route: $routeId" + systemNotificationService.create(code:'connection.route.exception', args:[routeId], kwargs:[exception:caughtException]) RouteDestroyJob.triggerNow([routeId:routeId as long]) } catch(Exception e) { e.printStackTrace() } } + def enableFconnection(Fconnection c) { + c.enabled = true + if(!c.save()) { + generateErrorSystemNotifications(c) + } + createRoutes(c) + } + + def disableFconnection(Fconnection c) { + destroyRoutes(c) + c.enabled = false + c.save(failOnError:true) + } + + private def generateErrorSystemNotifications(connectionInstance){ + def notificationText + connectionInstance.errors.allErrors.collect { error -> + notificationText = messageSource.getMessage(error, null) + systemNotificationService.create(code:'connection.error.onsave', args:[notificationText]) + }.join('\n') + } + private def logFail(c, ex) { ex.printStackTrace() log.warn("Error creating routes to fconnection with id $c?.id", ex) - LogEntry.log("Error creating routes to fconnection with name ${c?.name?: c?.id}") - createSystemNotification('connection.route.failNotification', [c?.id, c?.name?:c?.id], ex) + logService.handleRouteCreationFailed(c) + def link = /#" onclick="mediumPopup.editConnection(${c.id})/ + systemNotificationService.create(code:'connection.route.failNotification', + args:[link, c?.name?:c?.id], + kwargs:[exception:ex, connection:c]) } - private def createSystemNotification(code, args, exception=null) { - if(exception) args += [i18nUtilService.getMessage(code:'connection.error.'+exception.class.name.toLowerCase(), args:[exception.message])] - def text = i18nUtilService.getMessage(code:code, args:args) - def notification = SystemNotification.findByText(text) ?: new SystemNotification(text:text) - notification.read = false - notification.save(failOnError:true, flush:true) + private def isFailed(Fconnection c) { + // TODO I rather suspect that this method needs implementing + false } } diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/FmessageService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/FmessageService.groovy deleted file mode 100644 index 8710b5d38..000000000 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/FmessageService.groovy +++ /dev/null @@ -1,47 +0,0 @@ -package frontlinesms2 - -class FmessageService { - def messageSendService - - def move(messageList, activity, params) { - def messagesToSend = [] - messageList.each { messageInstance -> - if(messageInstance.isMoveAllowed()){ - messageInstance.ownerDetail = null - messageInstance.isDeleted = false - Trash.findByObject(messageInstance)?.delete(failOnError:true) - if (params.messageSection == 'activity') { - messageInstance.messageOwner?.removeFromMessages(messageInstance)?.save(failOnError:true) - activity.addToMessages(messageInstance) - if(activity.metaClass.hasProperty(null, 'autoreplyText') && activity.autoreplyText) { - params.addresses = messageInstance.src - params.messageText = activity.autoreplyText - def outgoingMessage = messageSendService.createOutgoingMessage(params) - outgoingMessage.save() - messagesToSend << outgoingMessage - activity.addToMessages(outgoingMessage) - activity.save() - } else if(activity instanceof Webconnection) { - activity.processKeyword(messageInstance, null) - }else if(activity instanceof Autoforward) { - activity.processKeyword(messageInstance, null) - } - } else if (params.ownerId && params.ownerId != 'inbox') { - messageInstance.messageOwner?.removeFromMessages(messageInstance)?.save(failOnError:true) - MessageOwner.get(params.ownerId).addToMessages(messageInstance).save(failOnError:true) - messageInstance.save() - } else { - messageInstance.with { - if(messageOwner) { - messageOwner.removeFromMessages(messageInstance).save(failOnError:true) - save(failOnError:true) - } - } - } - } - } - if(messagesToSend) { - MessageSendJob.defer(messagesToSend) - } - } -} diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/FrontlinesyncService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/FrontlinesyncService.groovy new file mode 100644 index 000000000..817658b31 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/FrontlinesyncService.groovy @@ -0,0 +1,128 @@ +package frontlinesms2 + +import org.springframework.transaction.annotation.Transactional +import org.apache.camel.Exchange +import grails.converters.JSON + +class FrontlinesyncService { + def fconnectionService + public static final int OUTBOUND_MESSAGE_SUCCESS_CODE = 2 + + def apiProcess(connection, controller) { + def data = controller.request.JSON + log.info "PARAMS : ${data}" + if(connection.secret && data.secret != connection.secret) { + return failure(controller, 'bad secret', 403) + } + + try { + data.payload.inboundTextMessages.each { e -> + sendMessageAndHeaders('seda:incoming-fmessages-to-store', + new TextMessage(inbound:true, + src:e.fromNumber, + text:e.text, + date:new Date(e.smsTimestamp)), + ['fconnection-id':connection.id]) + } + + data.payload.missedCalls.each { e -> + sendMessageAndHeaders('seda:incoming-missedcalls-to-store', + new MissedCall(inbound:true, + src:e.fromNumber, + date:new Date(e.callTimestamp)), + ['fconnection-id':connection.id]) + } + + data.payload.outboundTextMessageStatuses.each { msgStatus -> + def d = Dispatch.get(msgStatus.dispatchId) + if(msgStatus.deliveryStatus as int == OUTBOUND_MESSAGE_SUCCESS_CODE) { + d?.status = DispatchStatus.SENT + d?.dateSent = new Date() + } + else { + d?.status = DispatchStatus.FAILED + } + d?.save(failOnError: true) + } + + if(data.payload.config) { + def config = data.payload.config + updateSyncConfig(config, connection) + } + + def payload + def outgoingPayload = generateOutgoingResponse(connection) + connection.lastConnectionTime = new Date() + connection.save() + payload = (outgoingPayload as JSON) + controller.render text:payload + } catch(Exception ex) { + ex.printStackTrace() + failure(controller, ex.message) + } + } + + @Transactional + void processSend(Exchange x) { + def connection = FrontlinesyncFconnection.get(x.in.headers['fconnection-id']) + connection.addToQueuedDispatches(x.in.body) + connection.save(failOnError:true) + } + + @Transactional + private generateOutgoingResponse(connection) { + def responseMap = [:] + if(connection.sendEnabled) { + def q = connection.hasDispatches ? connection.queuedDispatches : [] + if(q) { + connection.removeDispatchesFromQueue() + responseMap.messages = q.collect { d -> + [to:d.dst, message:d.text, dispatchId:d.id] + } + } + } + if(!connection.configSynced) { + responseMap.config = generateSyncConfig(connection) + connection.configSynced = true + connection.save() + } + if(responseMap.keySet().size() == 0) { + responseMap.success = true + } + responseMap + } + + @Transactional + public updateSyncConfig(config, connection, markAsDirty = true){ + ["sendEnabled", "receiveEnabled", "missedCallEnabled"].each { + connection."$it" = config."$it" as boolean + } + if(config.checkIntervalIndex) { + connection.checkInterval = FrontlinesyncFconnection.checkIntervalOptions[config.checkIntervalIndex as int] + } + else if(config.checkInterval != null) { + connection.checkInterval = config.checkInterval as Integer + } + connection.configSynced = markAsDirty + if(connection.sendEnabled) { + fconnectionService.enableFconnection(connection) + } + else { + fconnectionService.destroyRoutes(connection) + } + connection.save() + } + + private generateSyncConfig(connection) { + def m = [:] + ["sendEnabled", "receiveEnabled", "missedCallEnabled", "checkInterval"].each { + m."$it" = connection."$it" + } + m + } + + private def failure(controller, message='ERROR', status=500) { + controller.render text:message, status:status + } +} + diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/GroupService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/GroupService.groovy new file mode 100644 index 000000000..e6f497619 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/GroupService.groovy @@ -0,0 +1,15 @@ +package frontlinesms2 + +class GroupService { + def delete(Group g) { + GroupMembership.deleteFor(g) + try { + g.delete(flush:true) + return true + } catch(Exception ex) { + ex.printStackTrace() + return false + } + } +} + diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/I18nUtilService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/I18nUtilService.groovy index 6a2a9ccf7..1597cfecc 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/I18nUtilService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/I18nUtilService.groovy @@ -1,5 +1,6 @@ package frontlinesms2 +import grails.util.Environment import org.springframework.web.servlet.LocaleResolver import org.springframework.web.servlet.support.RequestContextUtils import org.springframework.util.StringUtils @@ -23,13 +24,24 @@ class I18nUtilService { appSettingsService.set('language', language) } + private boolean isTranslationFilesEncodedAsJavePropertiesFiles() { + return Environment.current == Environment.PRODUCTION + } + synchronized def getAllTranslations() { if(!this.@allTranslations) { def translations = [:] new File(getRootDirectory()).eachFileMatch groovy.io.FileType.FILES, ~/messages(_\w\w)*\.properties$/, { file -> def filename = file.name def locale = getLocaleKey(filename) - def language = getLanguageName(filename) + def language + if(isTranslationFilesEncodedAsJavePropertiesFiles()) { + def prop = new Properties() + prop.load(file.newInputStream()) + language = prop.getProperty('language.name') + } else { + language = getLanguageName(filename) + } translations[locale] = language } this.allTranslations = translations.sort { it.value } @@ -45,7 +57,7 @@ class I18nUtilService { def f = new File(getRootDirectory(), filename) if(f.exists()) { def lang - try { f.eachLine { line -> + try { f.eachLine("utf-8") { line -> if(line.startsWith("language.name=")) { lang = (line - "language.name=").trim() throw new EOFException() @@ -58,14 +70,14 @@ class I18nUtilService { def getMessage(args) { // maybe we need Locale.setDefault(new Locale("en","US")) try { - def text = messageSource.getMessage(args.code, args.args as Object[], null) + return messageSource.getMessage(args.code, args.args as Object[], null) } catch(org.springframework.context.NoSuchMessageException _) { return args.code } } private String getRootDirectory() { - def fileURL = new File('web-app/WEB-INF/grails-app/i18n').path + return new File('web-app/WEB-INF/grails-app/i18n').path } } diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/IncomingMessageRouterService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/IncomingMessageRouterService.groovy index 99df8d27a..9919149f9 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/IncomingMessageRouterService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/IncomingMessageRouterService.groovy @@ -9,3 +9,4 @@ class IncomingMessageRouterService { camelContext.routes*.endpoint.findAll { it.endpointUri.endsWith('-fmessages-to-process') } } } + diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/IntelliSmsTranslationService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/IntelliSmsTranslationService.groovy index 7af688509..49a497443 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/IntelliSmsTranslationService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/IntelliSmsTranslationService.groovy @@ -6,26 +6,26 @@ import org.apache.camel.Exchange class IntelliSmsTranslationService implements Processor { static final String INTELLISMS_MESSAGING_ADDR = '@messaging.intellisoftware.co.uk' static final String INTERNATIONAL_SYMBOL = '+' - static transactional = false + static transactional = false //TODO please explain why this is not transactional void process(Exchange exchange) { - println("exchange ${exchange}") + log.info("exchange ${exchange}") def i = exchange.in - println("in: ${i}") + log.info("in: ${i}") if(isValidMessageSource(i.getHeader('From'))) { - Fmessage message = new Fmessage(inbound:true) + TextMessage message = new TextMessage(inbound:true) def emailBody = i.body def emailSubject = i.getHeader('Subject') def emailDate = i.getHeader('Date') message.src = INTERNATIONAL_SYMBOL + emailSubject.split(" ")[2] - println("src: ${message.src}") - println "emailBody: $emailBody" - println "emailSubject: $emailSubject" - println "emailDate: $emailDate" + log.info("src: ${message.src}") + log.info "emailBody: $emailBody" + log.info "emailSubject: $emailSubject" + log.info "emailDate: $emailDate" message.text = emailSubject message.date = Date.parse("EEE, dd MMM yyyy hh:mm:ss Z",emailDate) - println "message sent on ${message.date}" + log.info "message sent on ${message.date}" if(emailBody != null) { message.text = emailBody } @@ -37,7 +37,7 @@ class IntelliSmsTranslationService implements Processor { } private isValidMessageSource(from) { - println "from: $from" + log.info "from: $from" from.contains(INTELLISMS_MESSAGING_ADDR) } } diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/KeywordProcessorService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/KeywordProcessorService.groovy index 93328c556..8b39f98f9 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/KeywordProcessorService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/KeywordProcessorService.groovy @@ -1,19 +1,20 @@ package frontlinesms2 class KeywordProcessorService { - def process(Fmessage message) { + def process(TextMessage message) { def words = message.text?.trim().toUpperCase().split(/\s/) def topLevelMatch = Keyword.getFirstLevelMatch(words[0]) if(!topLevelMatch) topLevelMatch = Keyword.getFirstLevelMatch('') if(topLevelMatch) { def secondLevelMatch = null - if(words.length > 1) + if(words.length > 1) { secondLevelMatch = Keyword.getSecondLevelMatchInActivity(words[1], topLevelMatch.activity) + } if (secondLevelMatch) { secondLevelMatch.activity.processKeyword(message, secondLevelMatch) - } - else + } else { topLevelMatch.activity.processKeyword(message, topLevelMatch) + } } } } diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/LogService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/LogService.groovy new file mode 100644 index 000000000..f099161b9 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/LogService.groovy @@ -0,0 +1,12 @@ +package frontlinesms2 + +class LogService { + def handleRouteCreated(connection) { + def routes = connection.routeDefinitions + LogEntry.log("Created routes: ${routes*.id}") + } + + def handleRouteCreationFailed(connection) { + LogEntry.log("Error creating routes to fconnection with name ${connection?.name?: connection?.id}") + } +} diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/MessageSendService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/MessageSendService.groovy index 1c3848e77..6b615d67d 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/MessageSendService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/MessageSendService.groovy @@ -2,8 +2,9 @@ package frontlinesms2 class MessageSendService { static transactional = false + def recipientLookupService - def send(Fmessage m, Fconnection c=null) { + def send(TextMessage m, Fconnection c=null) { def headers = [:] if(c) headers['requested-fconnection-id'] = c.id m.save() @@ -12,10 +13,12 @@ class MessageSendService { } } - def retry(Fmessage m) { + def retry(TextMessage m) { def dispatchCount = 0 m.dispatches.each { dispatch -> if(dispatch.status == DispatchStatus.FAILED) { + dispatch.status = DispatchStatus.PENDING + dispatch.save() sendMessage('seda:dispatches', dispatch) ++dispatchCount } @@ -24,10 +27,15 @@ class MessageSendService { } def createOutgoingMessage(params) { - def message = new Fmessage(text:(params.messageText), inbound:false) - def addresses = [params.addresses].flatten() - null - addresses += getAddressesForContacts(params.contacts) - addresses += getAddressesForGroups([params.groups].flatten()) + def message = new TextMessage(text:(params.messageText), inbound:false) + def addresses = [] + if (params.recipients) { + addresses = recipientLookupService.getAddressesFromRecipientList(params.recipients) + } else { + addresses = [params.addresses].flatten() - null + addresses += getAddressesForContacts(params.contacts) + addresses += getAddressesForGroups([params.groups].flatten()) + } def dispatches = generateDispatches(addresses) dispatches.each { @@ -61,4 +69,3 @@ class MessageSendService { } } } - diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/MessageStorageService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/MessageStorageService.groovy index 86ff5dcf9..fd79fd2e2 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/MessageStorageService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/MessageStorageService.groovy @@ -4,13 +4,12 @@ import org.apache.camel.Exchange import org.apache.camel.Processor class MessageStorageService implements Processor { - public void process(Exchange ex) { - def message = ex.in.body - assert message instanceof Fmessage - message = message.id ? Fmessage.findById(message.id) : message - def conn = Fconnection.findById(ex.in.headers."${Fconnection.HEADER_FCONNECTION_ID}") + public void process(Exchange x) { + def message = x.in.body + assert message instanceof Interaction + message = message.id ? Interaction.findById(message.id) : message + message.connectionId = x.in.headers[Fconnection.HEADER_FCONNECTION_ID] as Long message.save(flush:true) - conn.addToMessages(message) - conn.save(flush:true) } } + diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/MobileNumberUtilService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/MobileNumberUtilService.groovy new file mode 100644 index 000000000..25902a32b --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/MobileNumberUtilService.groovy @@ -0,0 +1,29 @@ +package frontlinesms2 + +import com.google.i18n.phonenumbers.* + +class MobileNumberUtilService { + def getISOCountryCode(rawNumber) { + def phoneNumberUtil = PhoneNumberUtil.instance + def number + + try { + number = phoneNumberUtil.parse(rawNumber, null) + } catch (NoSuchElementException exception) { + return '' + } catch (NumberParseException exception) { + return '' + } + + phoneNumberUtil.getRegionCodeForNumber(number) + } + + def getFlagCSSClasses(phoneNumber, allowEmpty=false) { + if (!phoneNumber) { + return ((allowEmpty)?'':'flag flag-frontlinesms') + } + else { + return "flag flag-${getISOCountryCode(phoneNumber)?.toLowerCase() ?: 'frontlinesms'}" + } + } +} diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/NexmoService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/NexmoService.groovy new file mode 100644 index 000000000..b58152ed9 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/NexmoService.groovy @@ -0,0 +1,51 @@ +package frontlinesms2 + +import grails.converters.JSON + +import frontlinesms2.api.* + +class NexmoService { + def apiProcess(connection, controller) { + controller.render(generateApiResponse(connection, controller) as JSON) + } + + def generateApiResponse(connection, controller) { + //if(connection.secret && controller.params.secret != connection.secret) return failure() + + try { + def payload = handleIncoming(connection, controller.params) + return [payload:payload] + } catch(FrontlineApiException ex) { + return failure(ex) + } + } + + private def handleIncoming(connection, params) { + if(!connection.receiveEnabled) throw new FrontlineApiException("Receive not enabled for this connection") + if(!params.msisdn || params.text==null) throw new FrontlineApiException('Missing one or both of `msisdn` and `text` parameters') + /* parse received JSON with the following params: + msisdn : sender of incoming message + text : incoming message + */ + sendMessageAndHeaders('seda:incoming-fmessages-to-store', + new TextMessage(inbound:true, src:params.msisdn, text:params.text), + ['fconnection-id':connection.id]) + + def response = success() + return response + } + + private def failure(FrontlineApiException ex=null) { + if(ex) { + return [payload:[success:'false', error:ex.message]] + } else { + return [payload:[success:'false']] + } + } + + private def success(additionalContent=null) { + def responseMap = [success:'true'] + if(additionalContent) responseMap += additionalContent + return responseMap + } +} \ No newline at end of file diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/PollService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/PollService.groovy index 25efaa8fa..b97cf76f5 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/PollService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/PollService.groovy @@ -7,7 +7,7 @@ class PollService{ def saveInstance(poll, params) { // FIXME this should use withPoll to shorten and DRY the code, but it causes cascade errors as referenced here: // http://grails.1312388.n4.nabble.com/Cascade-problem-with-hasOne-relationship-td4495102.html - println "Poll Params ::${params}" + log.info "Poll Params ::${params}" poll.name = params.name ?: poll.name poll.autoreplyText = params.enableAutoreply? (params.autoreplyText ?: poll.autoreplyText): null poll.question = params.question ?: poll.question @@ -15,23 +15,35 @@ class PollService{ poll.editResponses(params) poll.keywords?.clear() poll.save(failOnError:true, flush:true) - println "#### Round 1 Save!!" + log.info "#### Round 1 Save!!" if(params.enableKeyword == "true"){ poll.editKeywords(params) - }else{ + } else { poll.noKeyword() } poll.save(failOnError:true) - println "#### Round 2 Save!!" - println "Keywords ${poll.keywords*.value}" + log.info "#### Round 2 Save!!" + log.info "Keywords ${poll.keywords*.value}" if(!params.dontSendMessage && !poll.archived && params.messageText) { - println "Sending a message as part of saving poll" + log.info "Sending a message as part of saving poll" def message = messageSendService.createOutgoingMessage(params) message.save() poll.addToMessages(message) - MessageSendJob.defer(message) + messageSendService.send(message) } poll.save(failOnError:true) poll } -} \ No newline at end of file + + def sendPollReply(pollInstance, message) { + def params = [:] + params.addresses = message.src + params.messageText = pollInstance.autoreplyText + def outgoingMessage = messageSendService.createOutgoingMessage(params) + pollInstance.addToMessages(outgoingMessage) + pollInstance.save(failOnError:true) + outgoingMessage.setMessageDetail(pollInstance, message.id) + outgoingMessage.save(failOnError:true) + messageSendService.send(outgoingMessage) + } +} diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/RecipientLookupService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/RecipientLookupService.groovy new file mode 100644 index 000000000..7eaf3fa5b --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/RecipientLookupService.groovy @@ -0,0 +1,116 @@ +package frontlinesms2 + +class RecipientLookupService { + private static final int MAX_PER_SECTION = 3 + + def contactSearchService + def i18nUtilService + + // TODO rename this method + def contactSearchResults(params) { + def selectedList = params.recipients + def contacts = objects(selectedList, Contact) + def groups = objects(selectedList, Group) + def smartgroups = objects(selectedList, SmartGroup) + def addresses = values(selectedList, 'address') + return [contacts:contacts, groups:groups, smartgroups:smartgroups, addresses:addresses] + } + + private def values(selectedList, shortName) { + selectedList.findAll { it.startsWith "$shortName-" }.collect { + it.split('-', 2)[1] + } + } + + private def ids(selectedList, clazz) { + values(selectedList, clazz.shortName)*.toLong()?: [0L] + } + + private def objects(selectedList, clazz) { + clazz.getAll(ids(selectedList, clazz)) - null + } + + def lookup(params) { + def query = "%${params.term.toLowerCase()}%" + def selectedSoFar = getSelectedSoFar(params) + def results = [contact:lookupContacts(query, ids(selectedSoFar, Contact)), + group:lookupGroups(query, ids(selectedSoFar, Group)), + smartgroup:lookupSmartgroups(query, ids(selectedSoFar, SmartGroup))].collect { k, v -> + if(v) [group:true, text:i18nUtilService.getMessage(code:"contact.search.$k"), items:v] } - null + def strippedNumber = stripNumber(params.term) + if (strippedNumber) { + results << [group:true, + text:i18nUtilService.getMessage([code:"contact.search.address"]), + items:[[value: "address-$strippedNumber", + text: "\"$strippedNumber\""]]] + } + return [query: params.term, results: results] + } + + private def getSelectedSoFar(params) { + def s = params.'selectedSoFar[]' + if(s) { + return s instanceof String? [s]: s + } + s = params.selectedSoFar + return s && s!='null'? s: '' + } + + private def stripNumber(mobile) { + def n = mobile?.replaceAll(/\D/, '') + if(mobile && mobile[0] == '+') n = '+' + n + n + } + + private def lookupContacts(query, alreadySelected=[]) { + contactSearchService.getContacts([searchString:query, max:MAX_PER_SECTION, exclude:alreadySelected]).collect { + [value: "contact-${it.id}", text: it.name] } + } + + private def lookupGroups(query, alreadySelected=[]) { + Group.findAllByNameIlikeAndIdNotInList(query, alreadySelected, [max:MAX_PER_SECTION]).collect { + [value: "group-${it.id}", text: "$it.name (${it.countMembers()})"] } + } + + private def lookupSmartgroups(query, alreadySelected=[]) { + SmartGroup.findAllByNameIlikeAndIdNotInList(query, alreadySelected, [max:MAX_PER_SECTION]).collect { + [value: "smartgroup-${it.id}", text: "$it.name (${it.countMembers()})"] } + } + + def stripPrefix = { it.tokenize('-')[1] } + + def getContacts = { recipients -> + [recipients].flatten().findAll { it.startsWith('contact') }.collect { Contact.get(stripPrefix(it)) }.findAll { it!=null } + } + + def getGroups = { recipients -> + [recipients].flatten().findAll { it.startsWith('group') }.collect { Group.get(stripPrefix(it)) }.flatten() + } + + def getSmartGroups = { recipients -> + [recipients].flatten().findAll { it.startsWith('smartgroup') }.collect { SmartGroup.get(stripPrefix(it)) }.flatten() + } + + def getManualAddresses = { recipients -> + log.info "############# $recipients" + [recipients].flatten().findAll { it.startsWith('address') }.collect { stripPrefix(it) } + } + + def getAddressesFromRecipientList(rawRecipients) { + def recipients = [rawRecipients].flatten() + def addresses = [] + def contactList = [] + def groupAddressList = [] + def smartGroupAddressList = [] + def manualAddressList = [] + + contactList = getContacts(recipients)*.mobile.flatten() - null + groupAddressList = getGroups(recipients)*.addresses.flatten() - null + smartGroupAddressList = getSmartGroups(recipients)*.addresses.flatten() - null + manualAddressList = getManualAddresses(recipients).flatten() + + addresses = contactList + groupAddressList + smartGroupAddressList + manualAddressList + addresses.flatten().unique() + } +} + diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/SmppTranslationService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/SmppTranslationService.groovy new file mode 100644 index 000000000..d975cc5f6 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/SmppTranslationService.groovy @@ -0,0 +1,44 @@ +package frontlinesms2 + +import org.apache.camel.Processor +import org.apache.camel.Exchange + +class SmppTranslationService implements Processor { + void process(Exchange exchange) { + def logWithPrefix = { t-> + log.info "SmppTranslationService.process ${t}" + } + logWithPrefix "ENTRY" + logWithPrefix ("exchange ${exchange}") + def i = exchange.in + logWithPrefix ("in: ${i}") + logWithPrefix ("in.headers: ${i.headers}") + logWithPrefix ("in.body: ${i.body}") + logWithPrefix ("in.getHeaders: ${i.getHeaders()}") + + //TODO allow messages is source is set + if(i.headers['CamelSmppSourceAddr']) { + TextMessage message = new TextMessage(inbound:true) + def messageBody = i.body + def messageSource = i.headers['CamelSmppSourceAddr'] + logWithPrefix "###### message-timestamp ${i.headers['CamelSmppDoneDate']}" + def messageDate = i.headers['CamelSmppDoneDate'] + + message.src = messageSource + message.text = messageBody + //TODO pick the value from Exchange + message.date = Date.parse("yyMMddHHmm",messageDate) + + logWithPrefix "message source is ${message.src}" + logWithPrefix "message body is ${message.text}" + logWithPrefix "message sent on ${message.date}" + + + exchange.in.body = message + logWithPrefix "IN::BODY ${exchange.in.body}" + } else { + exchange.setProperty(Exchange.ROUTE_STOP, Boolean.TRUE); + } + + } +} diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/SmslibTranslationService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/SmslibTranslationService.groovy index 688248a4f..b607ea1c0 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/SmslibTranslationService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/SmslibTranslationService.groovy @@ -6,14 +6,14 @@ import org.smslib.CStatusReportMessage import org.smslib.COutgoingMessage class SmslibTranslationService { - void toFmessage(Exchange exchange) { + void toTextMessage(Exchange exchange) { CIncomingMessage bod = exchange.in.body // Ignore CStatusReportMessages if(bod instanceof CStatusReportMessage) { return } else { - Fmessage message = new Fmessage(inbound:true) + TextMessage message = new TextMessage(inbound:true) message.src = bod.originator message.text = bod.text message.date = new Date(bod.date) @@ -25,14 +25,13 @@ class SmslibTranslationService { void toCmessage(Exchange exchange) { Dispatch d = exchange.in.body - Fmessage m = d.message + TextMessage m = d.message String address = d.dst String messageText = d.text?: '' def c = new COutgoingMessage(address, messageText) c.originator = m.src c.date = m.date.time - - exchange.out.body = c - exchange.out.setHeader('frontlinesms.dispatch.id', d.id) + exchange.in.body = c + exchange.in.setHeader('frontlinesms.dispatch.id', d.id) } } diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/SmssyncService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/SmssyncService.groovy index 51c0843f9..3079d9362 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/SmssyncService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/SmssyncService.groovy @@ -1,21 +1,29 @@ package frontlinesms2 import grails.converters.JSON - +import org.quartz.JobKey +import grails.plugin.quartz2.TriggerHelper import org.apache.camel.Exchange - import frontlinesms2.api.* +import org.springframework.transaction.annotation.Transactional +// TODO handle unscheduling of ReportSmssyncTimeoutJob as an event listener. class SmssyncService { - def processSend(Exchange x) { - println "SmssyncService.processSend() :: ENTRY" - println "SmssyncService.processSend() :: x=$x" - println "SmssyncService.processSend() :: x.in.body=$x.in.body" - println "SmssyncService.processSend() :: x.in.headers=${x.in?.headers}" + def i18nUtilService + + @Transactional + void processSend(Exchange x) { def connection = SmssyncFconnection.get(x.in.headers['fconnection-id']) connection.addToQueuedDispatches(x.in.body) + connection.hasDispatches = true connection.save(failOnError:true) - println "SmssyncService.processSend() :: EXIT" + } + + @Transactional + def reportTimeout(connection) { + SystemNotification.findOrCreateByText(i18nUtilService.getMessage( + code:'smssync.timeout', args:[connection.name, connection.timeout, connection.id])) + .save(failOnError:true) } def apiProcess(connection, controller) { @@ -26,7 +34,10 @@ class SmssyncService { if(connection.secret && controller.params.secret != connection.secret) return failure() try { + updateLastConnectionTime(connection) + connection.save(flush:true, failOnError: true) def payload = controller.params.task=='send'? handlePollForOutgoing(connection): handleIncoming(connection, controller.params) + startTimeoutCounter(connection) if(connection.secret) payload = [secret:connection.secret] + payload return [payload:payload] } catch(FrontlineApiException ex) { @@ -35,7 +46,28 @@ class SmssyncService { } } + def updateLastConnectionTime(connection) { + connection.lastConnectionTime = new Date() + } + + @Transactional + def startTimeoutCounter(connection) { + if (connection instanceof SmssyncFconnection && connection?.timeout > 0) { + ReportSmssyncTimeoutJob.unschedule("SmssyncFconnection-${connection.id}", "SmssyncFconnectionTimeoutJobs") + def sendTime = new Date() + use(groovy.time.TimeCategory) { + sendTime = sendTime + (connection.timeout).minutes + } + def trigger = TriggerHelper.simpleTrigger(new JobKey("SmssyncFconnection-${connection.id}", "SmssyncFconnectionTimeoutJobs"), sendTime, 0, 1, [connectionId:connection.id]) + trigger.name = "SmssyncFconnection-${connection.id}" + trigger.group = "SmssyncFconnectionTimeoutJobs" + ReportSmssyncTimeoutJob.schedule(trigger) + } + + } + private def handleIncoming(connection, params) { + if(!connection.enabled) throw new FrontlineApiException("Connection not enabled") if(!connection.receiveEnabled) throw new FrontlineApiException("Receive not enabled for this connection") if(!params.from || params.message==null) throw new FrontlineApiException('Missing one or both of `from` and `message` parameters'); @@ -47,7 +79,7 @@ class SmssyncService { sent_to -- the phone number the SMS was sent to sent_timestamp -- the timestamp the SMS was sent. In the UNIX timestamp format */ sendMessageAndHeaders('seda:incoming-fmessages-to-store', - new Fmessage(inbound:true, src:params.from, text:params.message), + new TextMessage(inbound:true, src:params.from, text:params.message), ['fconnection-id':connection.id]) def response = success() @@ -55,21 +87,22 @@ class SmssyncService { return response } + @Transactional private def handlePollForOutgoing(connection) { if(!connection.sendEnabled) throw new FrontlineApiException("Send not enabled for this connection") return success(generateOutgoingResponse(connection, true)) } + @Transactional private def generateOutgoingResponse(connection, boolean includeWhenEmpty) { def responseMap = [:] - def q = connection.queuedDispatches + def q = connection.hasDispatches ? connection.queuedDispatches : [] if(q || includeWhenEmpty) { responseMap.task = 'send' - connection.removeDispatchesFromQueue(q) - + connection.removeDispatchesFromQueue() responseMap.messages = q.collect { d -> d.status = DispatchStatus.SENT d.dateSent = new Date() diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/StatusIndicatorService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/StatusIndicatorService.groovy new file mode 100644 index 000000000..cb1a650cb --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/StatusIndicatorService.groovy @@ -0,0 +1,17 @@ +package frontlinesms2 + +import frontlinesms2.ConnectionStatus as CS + +class StatusIndicatorService { + def getColor() { + def status = Fconnection.list()*.status + if(CS.CONNECTED in status) { + return 'green' + } else if(CS.CONNECTING in status) { + return 'orange' + } else if(CS.FAILED in status) { + return 'red' + } + return 'grey' + } +} diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/SubscriptionService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/SubscriptionService.groovy index b2cf08a0d..525db52f1 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/SubscriptionService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/SubscriptionService.groovy @@ -1,10 +1,10 @@ package frontlinesms2 -import frontlinesms2.* - +// TODO TODO TODO this class needs a serious refactor as there's masses of copy.pasted code class SubscriptionService { + def messageSendService - def saveInstance(Subscription subscriptionInstance, params) { + def saveInstance(Subscription subscriptionInstance, params) { subscriptionInstance.group = Group.get(params.subscriptionGroup) if(subscriptionInstance.keywords) subscriptionInstance.keywords.clear() @@ -35,5 +35,99 @@ class SubscriptionService { subscriptionInstance.save(flush:true, failOnError: true) return subscriptionInstance } + + def doJoin(subscriptionOrActionStep, message) { + message.setMessageDetail(subscriptionOrActionStep, Subscription.Action.JOIN.toString()) + message.save(failOnError:true) + def group = subscriptionOrActionStep.group + def foundContact + withEachCorrespondent(message, { phoneNumber -> + foundContact = Contact.findByMobile(phoneNumber) + if(!foundContact) { + foundContact = new Contact(name:"", mobile:phoneNumber).save(failOnError:true) + group.addToMembers(foundContact); + } else { + if(!(foundContact.isMemberOf(group))){ + group.addToMembers(foundContact); + } + } + if(subscriptionOrActionStep instanceof Activity && subscriptionOrActionStep.joinAutoreplyText) { + sendAutoreplyMessage(foundContact, subscriptionOrActionStep.joinAutoreplyText, message) + } + }) + } + + def doLeave(subscriptionOrActionStep, message) { + message.setMessageDetail(subscriptionOrActionStep, Subscription.Action.LEAVE.toString()) + message.save(failOnError:true) + def group = subscriptionOrActionStep.group + def foundContact + withEachCorrespondent(message, { phoneNumber -> + foundContact = Contact.findByMobile(phoneNumber) + if(foundContact) { + if((foundContact.isMemberOf(group))){ + foundContact?.removeFromGroup(group) + } + if(subscriptionOrActionStep instanceof Activity && subscriptionOrActionStep.leaveAutoreplyText) { + sendAutoreplyMessage(foundContact, subscriptionOrActionStep.leaveAutoreplyText, message) + } + } + }) + } + + def doToggle(subscriptionOrActionStep, message) { + message.setMessageDetail(subscriptionOrActionStep, Subscription.Action.TOGGLE.toString()) + message.save(failOnError:true) + def group = subscriptionOrActionStep.group + def foundContact + withEachCorrespondent(message, { phoneNumber -> + foundContact = Contact.findByMobile(phoneNumber) + if(foundContact){ + if(foundContact.isMemberOf(group)) { + foundContact.removeFromGroup(group) + if(subscriptionOrActionStep instanceof Activity && subscriptionOrActionStep.leaveAutoreplyText) + sendAutoreplyMessage(foundContact, subscriptionOrActionStep.leaveAutoreplyText, message) + } else { + group.addToMembers(foundContact); + if(subscriptionOrActionStep instanceof Activity && subscriptionOrActionStep.joinAutoreplyText) + sendAutoreplyMessage(foundContact, subscriptionOrActionStep.joinAutoreplyText, message) + } + } else { + foundContact = new Contact(name:"", mobile:phoneNumber).save(failOnError:true) + group.addToMembers(foundContact); + if(subscriptionOrActionStep instanceof Activity && subscriptionOrActionStep.joinAutoreplyText) + sendAutoreplyMessage(foundContact, subscriptionOrActionStep.joinAutoreplyText, message) + } + }) + } + + def withEachCorrespondent(TextMessage message, Closure c) { + def phoneNumbers = [] + if (message.inbound) + phoneNumbers << message.src + else { + message.dispatches.each { d-> + phoneNumbers << d.dst + } + } + if (phoneNumbers.size() > 0) { + phoneNumbers.each { phoneNumber -> + c phoneNumber + } + } + } + + def sendAutoreplyMessage(Contact foundContact, autoreplyText, incomingMessage) { + def subscription = incomingMessage.messageOwner + def params = [:] + params.addresses = foundContact.mobile + params.messageText = autoreplyText + def outgoingMessage = messageSendService.createOutgoingMessage(params) + subscription.addToMessages(outgoingMessage) + subscription.save(failOnError:true) + outgoingMessage.setMessageDetail(subscription, incomingMessage.id) + outgoingMessage.save(failOnError:true) + messageSendService.send(outgoingMessage) + } } diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/SystemNotificationService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/SystemNotificationService.groovy index a438e48c2..dec523c11 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/SystemNotificationService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/SystemNotificationService.groovy @@ -1,16 +1,35 @@ package frontlinesms2 -import frontlinesms2.SystemNotificationService - class SystemNotificationService { - def i18nUtilService + def grailsApplication - def create(code, args, exception=null) { - if(exception) args += [i18nUtilService.getMessage(code:'connection.error.'+exception.class.name.toLowerCase(), args:[exception.message])] + def create(Map params) { + def code = params?.code + def args = params?.args + def kwargs = params?.kwargs ?: [:] + if(kwargs.exception) args += [i18nUtilService.getMessage(code:'connection.error.'+kwargs.exception.class.name.toLowerCase(), args:[kwargs.exception.message])] def text = i18nUtilService.getMessage(code:code, args:args) - def notification = SystemNotification.findByText(text) ?: new SystemNotification(text:text) + text = substituteLinks(text) + def blockedNotificationList = grailsApplication.config.frontlinesms.blockedNotificationList + if(!(blockedNotificationList && code in blockedNotificationList)) { getOrCreate(text, params?.topic) } + } + + private def getOrCreate(text, topic=null) { + def notification = SystemNotification.findOrCreateByText(text) + if(topic) { + SystemNotification.findAllByTopic(topic).each { + it.read = true + it.save() + } + notification.topic = topic + } notification.read = false - notification.save(failOnError:true, flush:true) + notification.save(flush:true) } -} \ No newline at end of file + + String substituteLinks(CharSequence input) { + input.replaceAll(/\[\[([^\[]*?)\]\]\(\((.*?)\)\)([^\)]|$)/, /$1<\/a>$3/) + } +} + diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/TextMessageInfoService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/TextMessageInfoService.groovy new file mode 100644 index 000000000..583e598c6 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/TextMessageInfoService.groovy @@ -0,0 +1,10 @@ +package frontlinesms2 + +import org.smslib.util.GsmAlphabet + +class TextMessageInfoService { + def getMessageInfos(String text) { + return TextMessageInfoUtils.getMessageInfos(text) + } +} + diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/TextMessageService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/TextMessageService.groovy new file mode 100644 index 000000000..9fe3feef1 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/TextMessageService.groovy @@ -0,0 +1,113 @@ +package frontlinesms2 + +import org.hibernate.criterion.CriteriaSpecification + +class TextMessageService { + def messageSendService + + def move(messageList, activity, params) { + def messagesToSend = [] + messageList.each { interactionInstance -> + if(interactionInstance.isMoveAllowed()){ + interactionInstance.clearAllDetails() + interactionInstance.isDeleted = false + Trash.findByObject(interactionInstance)?.delete(failOnError:true) + if (params.messageSection == 'activity') { + activity.move(interactionInstance) + activity.save(failOnError:true, flush:true) + } else if (params.ownerId && params.ownerId != 'inbox') { + interactionInstance.messageOwner?.removeFromMessages(interactionInstance)?.save(failOnError:true) + MessageOwner.get(params.ownerId).addToMessages(interactionInstance).save(failOnError:true) + interactionInstance.save() + } else { + interactionInstance.with { + if(messageOwner) { + messageOwner.removeFromMessages(interactionInstance).save(failOnError:true) + save(failOnError:true) + } + } + } + } + } + + if(messagesToSend) { + MessageSendJob.defer(messagesToSend) + } + } + + def search(search, params=[:]) { + def finteractionInstanceList = fmessageFilter(search) + def rawSearchResults = [] + def searchMessageInstanceTotal = 0 + def searchMessageInstanceList = [] + + if(finteractionInstanceList) { + rawSearchResults = TextMessage.search(finteractionInstanceList*.id) + } + + if(rawSearchResults) { + int offset = params.offset?.toInteger()?:0 + int max = params.max?.toInteger()?:50 + searchMessageInstanceList = rawSearchResults?.listDistinct(sort:'date', order:'desc', offset:offset, max:max) + searchMessageInstanceTotal = rawSearchResults?.count() + } + [interactionInstanceList:searchMessageInstanceList, interactionInstanceTotal:searchMessageInstanceTotal] + } + + private def fmessageFilter(search) { + def ids = TextMessage.withCriteria { + createAlias('dispatches', 'disp', CriteriaSpecification.LEFT_JOIN) + if(search.searchString) { + or { + ilike("text", "%${search.searchString}%") + ilike("src", "%${search.searchString}%") + ilike("disp.dst", "%${search.searchString}%") + } + } + if(search.contactString) { + def contactNumbers = Contact.findAllByNameIlike("%${search.contactString}%")*.mobile ?: [''] + or { + 'in'('src', contactNumbers) + 'in'('disp.dst', contactNumbers) + } + } + if(search.group) { + def groupMembersNumbers = search.group.addresses?: [''] //otherwise hibernate fail to search 'in' empty list + or { + 'in'('src', groupMembersNumbers) + 'in'('disp.dst', groupMembersNumbers) + } + } + if(search.status) { + if(search.status.toLowerCase() == 'inbound') eq('inbound', true) + else eq('inbound', false) + } + if(search.owners) { + 'in'("messageOwner", search.owners) + } + if(search.startDate && search.endDate) { + between('date', search.startDate, search.endDate) + } else if (search.startDate) { + ge('date', search.startDate) + } else if (search.endDate) { + le('date', search.endDate) + } + if(search.customFields.any { it.value }) { + // provide empty list otherwise hibernate fails to search 'in' empty list + def matchingContactsNumbers = Contact.findByCustomFields(search.customFields)*.mobile?: [''] + or { + 'in'('src', matchingContactsNumbers) + 'in'('disp.dst', matchingContactsNumbers) + } + } + if(!search.inArchive) { + eq('archived', false) + } + if(search.starredOnly) { + eq('starred', true) + } + eq('isDeleted', false) + // order('date', 'desc') removed due to http://jira.grails.org/browse/GRAILS-8162; please reinstate when possible + }*.refresh() // TODO this is ugly ugly, but it fixes issues with loading incomplete dispatches. Feel free to sort it out + } +} diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/TrashService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/TrashService.groovy index 2471fc51a..8a270adda 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/TrashService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/TrashService.groovy @@ -1,24 +1,26 @@ package frontlinesms2 class TrashService { + def i18nUtilService def emptyTrash() { - Fmessage.findAllByIsDeleted(true).each { + TextMessage.findAllByIsDeleted(true).each { def conn = it.receivedOn if(conn) { conn.removeFromMessages(it) conn.save() } } - Fmessage.findAllByIsDeleted(true)*.delete() + TextMessage.findAllByIsDeleted(true)*.delete() MessageOwner.findAllByDeleted(true)*.delete() Trash.findAll()*.delete() } def sendToTrash(object) { - if (object instanceof frontlinesms2.Fmessage) { + log.info "Deleting ${object}" + if (object instanceof frontlinesms2.Interaction) { object.isDeleted = true new Trash(displayName:object.displayName, - displayText:object.text, + displayText:(object instanceof frontlinesms2.TextMessage ? object.text.truncate(Trash.MAXIMUM_DISPLAY_TEXT_SIZE) : i18nUtilService.getMessage(code:'missedCall.displaytext', args:[object.displayName])), objectClass:object.class.name, objectId:object.id).save() object.save(failOnError:true, flush:true) @@ -37,11 +39,10 @@ class TrashService { } } - def restore(object) { + boolean restore(object) { Trash.findByObject(object)?.delete() object.restoreFromTrash() - object.save() - return true + return object.save() as boolean } } diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/UrlHelperService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/UrlHelperService.groovy new file mode 100644 index 000000000..07cff3b0d --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/UrlHelperService.groovy @@ -0,0 +1,13 @@ +package frontlinesms2 + +class UrlHelperService { + def i18nUtilService + + String getBaseUrl(request) { + def scheme = request.scheme.toLowerCase() + def port = request.serverPort + def explicitPort = (scheme == 'http' && port != 80) || (scheme == 'https' && port != 443) + def domain = (request.serverName == 'localhost' ? "<${i18nUtilService.getMessage(code:'localhost.ip.placeholder')}>" : request.serverName) + return "${scheme}://${domain}${explicitPort ? (':' + port ):''}" + } +} diff --git a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/WebconnectionService.groovy b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/WebconnectionService.groovy index 7b1fb175a..e69d4444c 100644 --- a/plugins/frontlinesms-core/grails-app/services/frontlinesms2/WebconnectionService.groovy +++ b/plugins/frontlinesms-core/grails-app/services/frontlinesms2/WebconnectionService.groovy @@ -1,59 +1,129 @@ package frontlinesms2 -import frontlinesms2.* import org.apache.camel.* import grails.converters.JSON import frontlinesms2.api.* - class WebconnectionService { + private static final REPLACEMENT_KEY = /[$][{]*[a-z_]*[}]/ + + def camelContext + def i18nUtilService def messageSendService - def preProcess(Exchange x) { - println "x: ${x}" - println "x.in: ${x.in}" - println "x.in.headers: ${x.in.headers}" - def webConn = Webconnection.get(x.in.headers.'webconnection-id') + private String getReplacement(String arg, TextMessage msg) { + arg = (arg - '${') - '}' + def c = Webconnection.subFields[arg] + return c ? c(msg) : arg + } + + private changeMessageOwnerDetail(activityOrStep, message, s) { + log.info "Status to set to $message is $s" + message.setMessageDetail(activityOrStep, s) + message.save(failOnError:true, flush:true) + log.info "Changing Status ${message.ownerDetail}" + } + + private def createRoute(webconnectionInstance, routes) { + try { + deactivate(webconnectionInstance) + camelContext.addRouteDefinitions(routes) + LogEntry.log("Created Webconnection routes: ${routes*.id}") + } catch(FailedToCreateProducerException ex) { + log.info ex + } catch(Exception ex) { + log.info ex + deactivate(webconnectionInstance) + } + } + + private TextMessage createTestMessage() { + TextMessage fm = new TextMessage(src:"0000", text:TextMessage.TEST_MESSAGE_TEXT, inbound:true) + fm.save(failOnError:true, flush:true) + } + + private def getWebConnection(Exchange x) { + def webConnection + if(x.in.headers.'webconnection-id') { + webConnection = Webconnection.get(x.in.headers.'webconnection-id') + } else if(x.in.headers.'webconnectionStep-id') { + webConnection = WebconnectionActionStep.get(x.in.headers.'webconnectionStep-id') + } + webConnection + } + + + String getProcessedValue(prop, msg) { + def val = prop.value + def matches = val.findAll(REPLACEMENT_KEY) + matches.each { match -> + val = val.replaceFirst(REPLACEMENT_KEY, getReplacement(match, msg)) + } + return val + } + + void preProcess(Exchange x) { + log.info "x: ${x}" + log.info "x.in: ${x.in}" + log.info "x.in.headers: ${x.in.headers}" + def webConn = getWebConnection(x) webConn.preProcess(x) } - def postProcess(Exchange x) { - println "x: ${x}" - println "x.in: ${x.in}" - println "x.in.headers: ${x.in.headers}" - println "### WebconnectionService.postProcess() ## headers ## ${x.in.headers}" - println "#### Completed postProcess #### ${x.in.headers.'fmessage-id'}" - def webConn = Webconnection.get(x.in.headers.'webconnection-id') - def message = Fmessage.get(x.in.headers.'fmessage-id') - changeMessageOwnerDetail(message, Webconnection.OWNERDETAIL_SUCCESS) + void postProcess(Exchange x) { + log.info "x: ${x}" + log.info "x.in: ${x.in}" + log.info "x.in.headers: ${x.in.headers}" + log.info "### WebconnectionService.postProcess() ## headers ## ${x.in.headers}" + log.info "#### Completed postProcess #### ${x.in.headers.'fmessage-id'}" + def webConn = getWebConnection(x) + def message = TextMessage.get(x.in.headers.'fmessage-id') + changeMessageOwnerDetail(webConn, message, Webconnection.OWNERDETAIL_SUCCESS) webConn.postProcess(x) } def handleException(Exchange x) { - def message = Fmessage.get(x.in.headers.'fmessage-id') - changeMessageOwnerDetail(message, Webconnection.OWNERDETAIL_FAILED) - println "### WebconnectionService.handleException() ## headers ## ${x.in.headers}" - println "Web Connection request failed with exception: ${x.in.body}" + def message = TextMessage.get(x.in.headers.'fmessage-id') + changeMessageOwnerDetail(getWebConnection(x), message, Webconnection.OWNERDETAIL_FAILED) + log.info "### WebconnectionService.handleException() ## headers ## ${x.in.headers}" + log.info "Web Connection request failed with exception: ${x.in.body}" log.info "Web Connection request failed with exception: ${x.in.body}" } - def handleFailed(Exchange x) { - } - - def handleCompleted(Exchange x) { + def createStatusNotification(Exchange x) { + def webConn + if(x.in.headers.'webconnection-id') { + webConn = Webconnection.get(x.in.headers.'webconnection-id') + } else if(x.in.headers.'webconnectionStep-id') { + webConn = WebconnectionActionStep.get(x.in.headers.'webconnectionStep-id') + } + def message = TextMessage.get(x.in.headers.'fmessage-id') + def text = i18nUtilService.getMessage(code:"webconnection.${message.ownerDetail}.label", args:[webConn.name]) + log.info "######## StatusNotification::: $text #########" + def notification = SystemNotification.findOrCreateByText(text) + notification.read = false + notification.save(failOnError:true, flush:true) } - def send(Fmessage message) { - println "## Webconnection.send() ## sending message # ${message}" + def doUpload(activityOrStep, message) { + log.info "## Webconnection.doUpload() ## uploading message # ${message}" def headers = [:] headers.'fmessage-id' = message.id - headers.'webconnection-id' = message.messageOwner.id - changeMessageOwnerDetail(message, Webconnection.OWNERDETAIL_PENDING) - sendMessageAndHeaders("seda:activity-webconnection-${message.messageOwner.id}", message, headers) + if(activityOrStep instanceof Webconnection) (headers.'webconnection-id' = activityOrStep.id) + else (headers.'webconnectionStep-id' = activityOrStep.id) + changeMessageOwnerDetail(activityOrStep, message, Webconnection.OWNERDETAIL_PENDING) + sendMessageAndHeaders("seda:activity-${activityOrStep.shortName}-${activityOrStep.id}", null, headers) } + def retryFailed(Webconnection c) { + TextMessage.findAllByMessageOwner(c).each { + if(it.ownerDetail == Webconnection.OWNERDETAIL_FAILED) + doUpload(c, it) + } + } + def saveInstance(Webconnection webconnectionInstance, params) { webconnectionInstance.keywords?.clear() webconnectionInstance.name = params.name @@ -63,7 +133,7 @@ class WebconnectionService { webconnectionInstance.keywords?.clear() webconnectionInstance.save(flush:true, failOnError:true) if (params.sorting == 'disabled') { - println "##### WebconnectionService.saveInstance() # removing keywords" + log.info "##### WebconnectionService.saveInstance() # removing keywords" } else if(params.sorting == 'global') { webconnectionInstance.addToKeywords(new Keyword(value:'', isTopLevel:true)) } else if(params.sorting == 'enabled') { @@ -73,29 +143,43 @@ class WebconnectionService { webconnectionInstance.save(failOnError:true, flush:true) } - private changeMessageOwnerDetail(Fmessage message, String s) { - message.ownerDetail = s - message.save(failOnError:true, flush:true) - println "Changing Status ${message.ownerDetail}" + def getStatusOf(Webconnection w) { + camelContext.routes.any { it.id ==~ /.*activity-${w.shortName}-${w.id}$/ } ? ConnectionStatus.CONNECTED : ConnectionStatus.FAILED } + private changeMessageOwnerDetail(TextMessage message, String s) { + message.setMessageDetail(message.messageOwner, s) + message.save(failOnError:true, flush:true) + log.info "Changing Status ${message.ownerDetail}" + } + def apiProcess(webcon, controller) { controller.render(generateApiResponse(webcon, controller)) } + def activate(activityOrStep) { + createRoute(activityOrStep, activityOrStep.routeDefinitions) + } + + def deactivate(activityOrStep) { + log.info "################ Deactivating Webconnection :: ${activityOrStep}" + camelContext.stopRoute("activity-${activityOrStep.shortName}-${activityOrStep.id}") + camelContext.removeRoute("activity-${activityOrStep.shortName}-${activityOrStep.id}") + } + def generateApiResponse(webcon, controller) { def message = controller.request.JSON?.message def recipients = controller.request.JSON?.recipients def secret = controller.request.JSON?.secret def errors = [invalid:[], missing:[]] - println "JSON IS ${controller.request.JSON}" - println "MESSAGE IS ${controller.request.JSON?.message}" - println "RECIPIENTS IS ${controller.request.JSON?.recipients}" + log.info "JSON IS ${controller.request.JSON}" + log.info "MESSAGE IS ${controller.request.JSON?.message}" + log.info "RECIPIENTS IS ${controller.request.JSON?.recipients}" //> Detect and return 401 (authentication) error conditions - if(!secret) + if(webcon.secret && !secret) return [status:401, text:"no secret provided"] - if(secret != webcon.secret) + if(webcon.secret && secret != webcon.secret) return [status:401, text:"invalid secret"] //> Detect and return 400 (invalid request) error conditions @@ -110,12 +194,12 @@ class WebconnectionService { if (errors.invalid) errorList << (errors.invalid.join(", ")) if (errors.missing) errorList << "missing required field(s): " + errors.missing.join(", ") def errorMessage = errorList.join(", ") - println "errorMessage ::: $errorMessage" + log.info "errorMessage ::: $errorMessage" return [status:400, text:errorMessage] } //> Populate destinations - println "evaluating the destinations for $recipients ...." + log.info "evaluating the destinations for $recipients ...." def groups = [] def addresses = [] recipients.each { @@ -143,16 +227,16 @@ class WebconnectionService { } groups = groups.unique() addresses = addresses.unique() - println "groups: $groups. Addresses: $addresses" + log.info "groups: $groups. Addresses: $addresses" //> Send message def m = messageSendService.createOutgoingMessage([messageText: message, addresses: addresses, groups: groups]) - println "I am about to send $m" - if(!m.dispatches) + log.info "I am about to send $m" + if(!m.dispatches) { return [status:400, text:"no recipients supplied"] - messageSendService.send(m) + } webcon.addToMessages(m) - webcon.save(failOnError: true) + messageSendService.send(m) "message successfully queued to send to ${m.dispatches.size()} recipient(s)" } } diff --git a/plugins/frontlinesms-core/grails-app/taglib/frontlinesms2/FsmsTagLib.groovy b/plugins/frontlinesms-core/grails-app/taglib/frontlinesms2/FsmsTagLib.groovy index 7a7df3b01..2754cfbc0 100644 --- a/plugins/frontlinesms-core/grails-app/taglib/frontlinesms2/FsmsTagLib.groovy +++ b/plugins/frontlinesms-core/grails-app/taglib/frontlinesms2/FsmsTagLib.groovy @@ -2,6 +2,9 @@ package frontlinesms2 import org.springframework.web.servlet.support.RequestContextUtils import org.codehaus.groovy.grails.web.taglib.exceptions.GrailsTagException +import frontlinesms2.CoreAppInfoProviders as CAIP +import org.codehaus.groovy.grails.web.pages.discovery.GrailsConventionGroovyPageLocator +import org.apache.commons.math.random.RandomDataImpl; class FsmsTagLib { static namespace = 'fsms' @@ -9,6 +12,17 @@ class FsmsTagLib { def appSettingsService def expressionProcessorService def grailsApplication + def i18nUtilService + def statusIndicatorService + GrailsConventionGroovyPageLocator groovyPageLocator + + def info = { att -> + def cssClass = 'info' + if(att.class) cssClass += ' ' + att.class + out << "

" + out << g.message(code:att.message) + out << '

' + } def ifAppSetting = { att, body -> if(Boolean.parseBoolean(appSettingsService[att.test])) { @@ -53,7 +67,10 @@ class FsmsTagLib { def tab = { att, body -> def con = att.controller out << '
  • ${g.message(code:att.title)}" + out << "
    " + if(att.info) out << info([message:att.info]) + out << "
      " + att.values.each { key, checked -> + def label = g.message(code:att.label + '.' + key) + def itemAttributes = [checked:checked, name:key, value:true] + out << '
    • ' + } + out << body() + out << '
    ' + } + def radioGroup = { att -> - def values = att.values.tokenize(',')*.trim() - def labels = att.labels.tokenize(',')*.trim() + def values = att.remove('values') + values = values instanceof String? values.tokenize(',')*.trim(): values + def labels = att.labels? att.remove('labels').tokenize(',')*.trim(): null + def isChecked = { v -> v == att.checked } + if(att.title) { + def hTag = att.solo == 'true'? 'h2': 'h3' + out << "<$hTag>${g.message(code:att.title)}" + att.remove('title') + } + if(att.info) out << info([message:att.info]) + def labelPrefix = att.remove('labelPrefix')?: '' + def labelSuffix = att.remove('labelSuffix')?: '' + def descriptionPrefix = att.remove('descriptionPrefix')?: '' + def descriptionSuffix = att.remove('descriptionSuffix')?: '' + def hasDescription = descriptionPrefix || descriptionSuffix + def useImages = att.remove('useImages') + def cssClasses = ['select', 'radio'] + if(!hasDescription) cssClasses << 'no-description' + out << "
    " + out << "
      " values.eachWithIndex { value, i -> - def label = labels[i] + def labelCode = labels? labels[i]: g.message(code:labelPrefix + value + labelSuffix) + def label = g.message(code:labelCode) def id = att.name + '-' + i - def itemAttributes = att + [value:value, checked:att.checked==value, id:id] - out << '
      ' + def itemAttributes = att + [value:value, checked:isChecked(value), id:id] + out << '
    • ' + out << '' } + out << '
    ' } /** FIXME use of this taglib should be replaced with CSS white-space:nowrap; */ @@ -93,14 +154,26 @@ class FsmsTagLib { def render = { att -> boolean rendered = false def plugins = grailsApplication.config.frontlinesms.plugins + def templateId = att.remove 'id' + def type = att.remove 'type' + if(type == 'sanchez') { + def runtimeVars = att.remove('runtimeVars')?.split(",")*.trim() + if(runtimeVars) { + if(!att.model) att.model = [:] + runtimeVars.each { att.model[it] = "{{$it}}" } + } + } ([null] + plugins).each { plugin -> if(!rendered) { try { att.plugin = plugin + if(type == 'sanchez') out << '' rendered = true - } catch(GrailsTagException ex) { - if(ex.message.startsWith("Template not found")) { + } catch(Exception ex) { + if((ex instanceof GrailsTagException && ex.message.startsWith('Template not found')) || + (ex instanceof IllegalArgumentException && ex.message == 'Argument [txt] cannot be null or blank')) { // Thanks for not subclassing your exceptions, guys! log.debug "Could not render $plugin:$att.template", ex } else { @@ -112,11 +185,26 @@ class FsmsTagLib { if(!rendered) throw new GrailsTagException("Failed to render [att=$att, plugins=${grailsApplication.config.frontlinesms.plugins}]") } - private def templateExists(name, plugin) { - // FIXME need to use `plugin` variable when checking for resource - def fullUri = grailsAttributes.getTemplateUri(name, request) - def resource = grailsAttributes.pagesTemplateEngine.getResourceForUri(fullUri) - return resource && resource.file && resource.exists() + def templateElseBody = { att, body -> + try { + out << render(att) + } + catch(GrailsTagException gte) { + out << body() + } + } + + def interactionTemplate = { att, body -> + def interactionType = (controllerName == 'missedCall' ? 'missedCall' : 'message') + def requestedTemplate = att.template + // TODO find a way to reuse templateElseBody here - this re-implementation is to work around fact that 2nd render would be + // rendered before passing to templateElseBody, which could cause failure + try { + out << render(att + [template: "/$interactionType/$requestedTemplate"]) + } + catch(GrailsTagException gte) { + out << render(att + [template: "/interaction/$requestedTemplate"]) + } } def i18nBundle = { @@ -127,32 +215,38 @@ class FsmsTagLib { // TODO this could likely be streamlined by using i18nUtilService.getCurrentLanguage(request) ['', "_${locale.language}", "_${locale.language}_${locale.country}", - "_${locale.language}_${locale.country}_${locale.variant}"].each { - out << "" } + "_${locale.language}_${locale.country}_${locale.variant}"].each { localeSuffix -> + if(i18nUtilService.allTranslations.containsKey(localeSuffix - '_')) { + def link = g.resource plugin:bundle, dir:'i18n', file:"messages${localeSuffix}.js" + out << "\n" + } + } } } - + def confirmTable = { att -> - out << '' - out << confirmTypeRow(att) def fields = getFields(att) - if(fields instanceof Map) { - generateConfirmSection(att, fields) - } else { - fields.each { out << confirmTableRow(att + [field:it.trim()]) } + if (fields) { + out << '
    ' + out << confirmTypeRow(att) + if(fields instanceof Map) { + generateConfirmSection(att, fields) + } else { + fields.each { out << confirmTableRow(att + [field:it.trim()]) } + } + out << '
    ' } - out << '' } - + def confirmTypeRow = { att -> out << '' out << '' - out << g.message(code:"${att.instanceClass.simpleName.toLowerCase()}.type.label") + out << g.message(code:"${att.instanceClass.shortName}.type.label") out << '' out << '' out << '' } - + def confirmTableRow = { att -> out << '' out << '' @@ -162,8 +256,9 @@ class FsmsTagLib { out << '' } - def activityConfirmTable = { att -> + def activityConfirmTable = { att, body -> out << '' + out << body() def fields = getFields(att) fields.each { out << activityConfirmTableRow(att + [field:it.trim()]) } out << '
    ' @@ -177,20 +272,43 @@ class FsmsTagLib { out << '' out << '' } - + def inputs = { att -> if(att.table) out << '' + if(att.list) out << "
    " def fields = getFields(att) - if(fields instanceof Map) { - generateSection(att, fields) - } else { - fields.each { - out << input(att + [field:it]) + if(!hasCustomConfigTemplate(att)) { + if(fields instanceof Map) { + generateSection(att, fields) + } else { + def values = att.values + def types = att.types + ['values', 'types'].each { att.remove(it) } + fields.eachWithIndex { field, i -> + def extraAttributes = [field:field] + if(values) { + extraAttributes.val = values[i] + if(types && types[i]) { + extraAttributes[types[i]] = true + } + } + out << input(att + extraAttributes) + } + } + if(att.submit) { + if(att.table) out << '
    ' } + } else { + out << render(template: "/fconnection/${att.instanceClass?.shortName}/config") } if(att.table) out << '
    ' + if(att.list) out << "
    " + out << g.submitButton(class:'btn', value:g.message(code:att.submit), name:att.submitName?:'submit') + if(att.list) out << '
    ' + if(att.table) out << '
    ' + if(att.list) out << '' } - + def input = { att, body -> def groovyKey = att.field // TODO remove references to att.instanceClass and make sure that all forms in app @@ -198,36 +316,50 @@ class FsmsTagLib { // specially for the view def instanceClass = att.instance?.getClass()?: att.instanceClass def htmlKey = (att.fieldPrefix!=null? att.fieldPrefix: instanceClass?instanceClass.shortName:'') + att.field - def val - if(att.instance) { - val = att.instance?."$groovyKey" - } else { - val = instanceClass?.defaultValues?."$groovyKey"?:null - } - - - ['instance', 'instanceClass'].each { att.remove(it) } - att += [name:htmlKey, value:val] - if(att.table) out << '' + def labelKey = (att.labelPrefix!=null? att.labelPrefix: instanceClass?instanceClass.shortName+'.':'') + att.field + '.label' + def validationRequired = instanceClass != null + + if(att.list) out << "
    " + else if(att.table) out << '' else out << '
    ' - out << '' - if(att.table) out << '' - if(att.class) att.class += addValidationCss(instanceClass, att.field) - else att.class = addValidationCss(instanceClass, att.field) - - if(att.password || isPassword(instanceClass, groovyKey)) { - out << g.passwordField(att) - } else if(instanceClass.metaClass.hasProperty(null, groovyKey)?.type.enum) { - out << g.select(att + [from:instanceClass.metaClass.hasProperty(null, groovyKey).type.values(), - noSelection:[null:'- Select -']]) - } else if(isBooleanField(instanceClass, groovyKey)) { - out << g.checkBox(att) - } else out << g.textField(att) - out << body() - if(att.table) { + if((att.instanceClass?.configFields && !groovyKey.startsWith("info-")) || (!att.instanceClass?.configFields && att.field)) { + def val + if(att.val) { + val = att.val + } else if(att.instance) { + val = att.instance?."$groovyKey" + } else { + val = instanceClass?.defaultValues?."$groovyKey"?:null + } + + ['instance', 'instanceClass'].each { att.remove(it) } + att += [name:htmlKey, value:val] + out << '' + if(att.table) out << '' + if(validationRequired) { + if(att.class) att.class += addValidationCss(instanceClass, att.field) + else att.class = addValidationCss(instanceClass, att.field) + } + + if(att.password || isPassword(instanceClass, groovyKey)) { + out << g.passwordField(att) + } else if(getMetaClassProperty(instanceClass, groovyKey)?.type?.enum) { + out << g.select(att + [from:getMetaClassProperty(instanceClass, groovyKey).type.values(), + noSelection:[null:'- Select -']]) + } else if(att.isBoolean || isBooleanField(instanceClass, groovyKey)) { + out << g.checkBox(att) + } else out << g.textField(att) + out << body() + } else if(att.instanceClass?.configFields) { + out << "
    ${g.message(code:"${att.instance?.class?.shortName?:instanceClass?.shortName?:'connection'}.${groovyKey}").markdownToHtml()}
    " + } + if(att.list) out << "
    " + else if(att.table) { out << '' } else { out << '
    ' @@ -263,15 +395,48 @@ class FsmsTagLib { out << '
    ' } + def recipientSelector = { att -> + if (att.explanatoryText) { + out << '

    ' + out << i18nUtilService.getMessage([code:'contact.search.helptext']) + out << '

    ' + } + out << '' + } + def unsubstitutedMessageText = { att -> out << expressionProcessorService.getUnsubstitutedDisplayText(att.messageText) } def trafficLightStatus = { att -> out << '' } @@ -283,25 +448,27 @@ class FsmsTagLib { def datePicker = { att -> def name = att.name def clazz = att.remove('class') + clazz = clazz? "date-picker $clazz": 'date-picker' att.value = att.value ?: 'none' att.precision = "day" att.noSelection = ['none':''] - out << '' + out << "
    " out << g.datePicker(att) out << "" out << '
    ' } def quickMessage = { att -> - att.controller = "quickMessage" - att.action = "create" - att.id = "quick_message" - att.onLoading = "showThinking();" - // FIXME activity-specific code should not be inside this file - att.onSuccess = "hideThinking(); mediumPopup.launchMediumWizard(i18n('wizard.quickmessage.title'), data, i18n('wizard.send'), true); mediumPopup.selectSubscriptionGroup(${att.groupId});" + def popupCall = "mediumPopup.launchMediumWizard(i18n('wizard.quickmessage.title'), data, i18n('wizard.send'), true);" + def params = [ groupList:(att.groupList?:'') ] + att << [controller:'quickMessage', action:'create', id:'quick_message', popupCall:popupCall, params:params] def body = "${g.message(code:'fmessage.quickmessage')}" + out << fsms.popup(att, body) + } + + def popup = { att, body -> + att << [onLoading:"showThinking();", onSuccess:"hideThinking(); ${att.popupCall}"] + att.remove('popupCall') out << g.remoteLink(att, body) } @@ -314,9 +481,17 @@ class FsmsTagLib { } def submenu = { att, body -> + def icon + def iconMap = ['messages': 'envelope', 'missedCalls': 'phone', 'activities': 'comments', 'folders':'folder-open', 'contacts':'user', 'groups':'users', 'smartgroups':'cog'] + if(att.class in iconMap.keySet()) { + icon = iconMap[att.class] + } out << '
  • ' if(att.code) { out << '

    ' + if(icon) { + out << "" + } out << g.message(code:att.code) out << '

    ' } @@ -329,7 +504,12 @@ class FsmsTagLib { def menuitem = { att, body -> def classlist = att.class?:"" classlist += att.selected ? " selected" : "" - out << '
  • ' + out << '
  • ' if (att?.bodyOnly) { out << body() @@ -339,53 +519,158 @@ class FsmsTagLib { def msgargs = att.msgargs def p = att.params out << g.link(controller:att.controller, action:att.action, params:p, id:att.id) { - out << (att.string ? att.string : g.message(code:msg, args:msgargs)) + out << "${(att.string ? att.string : g.message(code:msg, args:msgargs))}" + if (body) { + out << body() + } } } out << '
  • ' } - + + def unreadCount = { att, body -> + def val = att.unreadCount + out << "" + val + "" + } + + def pendingCount = { att, body -> + def val = att.pendingCount + out << "" + val + "" + } + + def select = { att, body -> + // add the no-selection option to the list if required + if(!att.hideNoSelection && att.noSelection && att.value != null) { + def key = (att.noSelection.keySet() as List).first() + def value = (att.noSelection.values() as List).first() + if(att.optionKey && att.optionValue) { + if(att.optionKey && att.optionKey instanceof Closure || att.optionValue instanceof Closure) { + att.from = [[key:key, value:value]] + att.from + } else { + att.from = [[(att.optionKey):key, (att.optionValue):value]] + att.from + } + } else { + if(att.keys) att.keys = [key] + att.keys + att.from = [value] + att.from + } + } + out << g.select(att, body) + } + + def messageComposer = { att, body -> + out << '
    ' + // + def placeholder = g.message(code:att.placeholder) + out << g.textArea( + name:att.name, + value:att.value, + placeholder:placeholder, + rows:att.rows?:"3", + id:att.textAreaId) + + out << '
    ' + out << '
    0

    ' + def magicWandAttributes = [controller:att.controller, target:att.target, fields:att.fields, hidden:false] + out << magicWand(magicWandAttributes) + out << '
    ' + out << '
    ' + + out << '
    ' + } + + def step = { att, body -> + out << render(template:'/customactivity/step', model:[stepId:att.stepId, type:att.type, body:body]) + } + + def fieldErrors = { att, body -> + def errors = att.bean?.errors?.allErrors.findAll{ it.field == att.field } + def errorMessages = errors.collect { message(error:it) }.join(att.delimeter?:" ") + if (errors && errorMessages) { + out << "" + } + } + + def contactWarning = { att, body -> + def warningType = att.warningType + out << "
    " + } + + def frontlineSyncPasscode = { att, body -> + def connection = att.connection + def passcode + if(connection) { + passcode = connection.secret + } else { + def randomData = new RandomDataImpl() + passcode = randomData.nextInt(1000, 9999) + } + out << g.hiddenField(name:'frontlinesyncsecret', value:passcode) + } + private def getFields(att) { def fields = att.remove('fields') if(!fields) fields = att.instanceClass?.configFields if(fields instanceof String) fields = fields.tokenize(',')*.trim() return fields } - + + private def hasCustomConfigTemplate(att) { + return (groovyPageLocator.findTemplateByPath("/fconnection/${att.instanceClass?.shortName}/config") != null) + } + private def getFieldLabel(clazz, fieldName) { - g.message(code:"${clazz.simpleName.toLowerCase()}.${fieldName}.label") + g.message(code:"${clazz.shortName}.${fieldName}.label") } private def getActivityFieldLabel(att) { - g.message(code:"${att.instanceClass?.shortName.toLowerCase()}.${att.type}.${att.field}.label") + g.message(code:"${att.instanceClass.shortName}.${att.type}.${att.field}.label") } - + private def isPassword(instanceClass, groovyKey) { - return instanceClass.metaClass.hasProperty(null, 'passwords') && + return getMetaClassProperty(instanceClass, 'passwords') && groovyKey in instanceClass.passwords } - + private def isBooleanField(instanceClass, groovyKey) { - return instanceClass.metaClass.hasProperty(null, groovyKey).type in [Boolean, boolean] + return getMetaClassProperty(instanceClass, groovyKey)?.type in [Boolean, boolean] } - + + private def getMetaClassProperty(clazz, groovyKey) { + if(clazz) { + return clazz.metaClass.hasProperty(null, groovyKey) + } + } + private def generateSection(att, fields) { def keys = fields.keySet() keys.each { key -> if(fields[key]) { out << "
    " - out << "
    " + if(att.list) out << "
    " + else out << "
    " out << "" out << input(att + [field:key]) out << "" - + //handle subsections within a subsection if(fields[key] instanceof LinkedHashMap) { generateSection(att, fields[key]) } else { fields[key].each {field -> if(field instanceof String) { - out << input(att + [field:field] + [class:"$key-subsection-member"]) + out << input(att + [field:field] + [class:"$key-subsection-member"]) } } } @@ -394,17 +679,16 @@ class FsmsTagLib { } else { out << input(att + [field:key]) } - } } - + private def generateConfirmSection(att, fields) { def keys = fields.keySet() keys.each { key -> if(fields[key]) { out << "
    " - out << confirmTableRow(att + [field:key]) - + if(!key.startsWith("info-")) out << confirmTableRow(att + [field:key]) + //handle subsections within a subsection if(fields[key] instanceof LinkedHashMap) { generateConfirmSection(att, fields[key]) @@ -419,16 +703,24 @@ class FsmsTagLib { } else { out << confirmTableRow(att + [field:key]) } - } } + def inlineEditable = { att -> + def domainClassName = att.instance.class.name + def currentValue = att.instance."${att.field}" + def currentValueSize = currentValue.length() + 20 + def classes = "inline-editable ${att.class ?: ''}" + out << "" + out << '' + } + private def isRequired(instanceClass, field) { - !instanceClass.constraints[field].nullable + !instanceClass.constraints[field].blank } private def isInteger(instanceClass, groovyKey) { - return instanceClass.metaClass.hasProperty(null, groovyKey).type in [Integer, int] + getMetaClassProperty(instanceClass, groovyKey)?.type in [Integer, int] } private def addValidationCss(instanceClass, field) { @@ -443,3 +735,4 @@ class FsmsTagLib { cssClasses } } + diff --git a/plugins/frontlinesms-core/grails-app/views/_css.gsp b/plugins/frontlinesms-core/grails-app/utils/.gitkeep similarity index 100% rename from plugins/frontlinesms-core/grails-app/views/_css.gsp rename to plugins/frontlinesms-core/grails-app/utils/.gitkeep diff --git a/plugins/frontlinesms-core/grails-app/views/_flash.gsp b/plugins/frontlinesms-core/grails-app/views/_flash.gsp index 0189f1c49..2a0a6c98d 100644 --- a/plugins/frontlinesms-core/grails-app/views/_flash.gsp +++ b/plugins/frontlinesms-core/grails-app/views/_flash.gsp @@ -1,10 +1,4 @@ - -
    - ${flash.message} - x -
    -
    - +
    diff --git a/plugins/frontlinesms-core/grails-app/views/_head.gsp b/plugins/frontlinesms-core/grails-app/views/_head.gsp index e50c342c8..5b5d5bb8b 100644 --- a/plugins/frontlinesms-core/grails-app/views/_head.gsp +++ b/plugins/frontlinesms-core/grails-app/views/_head.gsp @@ -2,12 +2,12 @@ diff --git a/plugins/frontlinesms-core/grails-app/views/autoforward/_validate.gsp b/plugins/frontlinesms-core/grails-app/views/autoforward/_validate.gsp index 2dc5cc0bd..cc5095078 100644 --- a/plugins/frontlinesms-core/grails-app/views/autoforward/_validate.gsp +++ b/plugins/frontlinesms-core/grails-app/views/autoforward/_validate.gsp @@ -1,7 +1,6 @@ function initializePopup() { - $("#messageText").val("${activityInstanceToEdit.sentMessageText}"); $("#messageText").trigger("keyup"); checkSavedContactsAndGroups(); @@ -27,20 +26,13 @@ return validator.element('#messageText'); }; - var recipientTabValidation = function() { - var valid = false; - addAddressHandler(); - valid = ($('input[name=addresses]:checked').length > 0) || ($('input[name=groups]:checked').length > 0); - return valid; - }; - var confirmTabValidation = function() { return validator.element('input[name=name]'); }; mediumPopup.addValidation('activity-generic-sorting', keyWordTabValidation); mediumPopup.addValidation('autoforward-create-message', messageTextTabValidation); - mediumPopup.addValidation('autoforward-recipients', recipientTabValidation); + mediumPopup.addValidation('autoforward-recipients', recipientSelecter.validateDeferred); mediumPopup.addValidation('autoforward-confirm', confirmTabValidation); $("#tabs").bind("tabsshow", function(event, ui) { @@ -49,28 +41,9 @@ } function updateConfirmationMessage() { - var regx = new RegExp("/\(\d+\)/", "g"); var autoforwardText = $('#messageText').val().htmlEncode(); - var contactInputIds = $('input[name=addresses]:checked').map(function() { return this.id; }); - var contactsList = contactInputIds.map(function(){ - if($("label[for="+ this +"]").text().length > 0){ - return $("label[for="+ this +"]").text(); - } - }); - var manualContactsList = $('li.manual.contact input').map(function() { return this.value; }); - var contacts = jQuery.merge(jQuery.makeArray(manualContactsList), jQuery.makeArray(contactsList)).join(', '); - if (contacts.length == 0) { - contacts = i18n('autoforward.contacts.none'); - } - - var groupInputIds = $('input[name=groups]:checked').map(function() { return this.id; }); - var groupsList = groupInputIds.map(function(){ return $("label[for="+ this +"]").text() }); - var groups = jQuery.makeArray(groupsList).join(', ').replace(/\s*\(\d+\)/g, ""); - if (groups.length == 0) { - groups = i18n('autoforward.groups.none'); - } - var keywordstate = $("input:radio[name=sorting]:checked").val(); + if(keywordstate === "enabled") { var keywords = $('#keywords').val().toUpperCase(); $("#keyword-confirm").html('

    ' + keywords + '

    '); @@ -78,10 +51,8 @@ else { $("#keyword-confirm").html('

    ' + i18n("autoforward." + keywordstate + ".keyword") + '

    '); } - + $("#autoforward-confirm-recipient-count").html('

    ' + recipientSelecter.getRecipientCount() + '

    '); $("#autoforward-confirm-messagetext").html('

    ' + autoforwardText + '

    '); - $("#autoforward-confirm-contacts").html('

    ' + contacts + '

    '); - $("#autoforward-confirm-groups").html('

    ' + groups + '

    '); } function checkSavedContactsAndGroups(){ @@ -95,5 +66,5 @@ $("#recipients-list input[value='smartgroup-${g.id}']").trigger("click"); } -
    + diff --git a/plugins/frontlinesms-core/grails-app/views/autoreply/_validate.gsp b/plugins/frontlinesms-core/grails-app/views/autoreply/_validate.gsp index a7d8bb7a9..b5692d975 100644 --- a/plugins/frontlinesms-core/grails-app/views/autoreply/_validate.gsp +++ b/plugins/frontlinesms-core/grails-app/views/autoreply/_validate.gsp @@ -1,14 +1,16 @@ function initializePopup() { + var validator, keyWordTabValidation, messageTextTabValidation, confirmTabValidation; + - $("#messageText").val("${activityInstanceToEdit.autoreplyText}"); + $("#messageText").val("${activityInstanceToEdit.autoreplyText.escapeForJavascript()}"); $("#messageText").trigger("keyup"); aliasCustomValidation(); genericSortingValidation(); - var validator = $("#create_autoreply").validate({ + validator = $("#create_autoreply").validate({ errorContainer: ".error-panel", rules: { messageText: { required:true }, @@ -16,17 +18,17 @@ } }); - var keyWordTabValidation = function() { + keyWordTabValidation = function() { if(!isGroupChecked("blankKeyword")){ return validator.element('#keywords'); } else return true; }; - var messageTextTabValidation = function() { + messageTextTabValidation = function() { return validator.element('#messageText'); }; - var confirmTabValidation = function() { + confirmTabValidation = function() { return validator.element('input[name=name]'); }; @@ -40,9 +42,10 @@ } function updateConfirmationMessage() { - var autoreplyText = $('#messageText').val().htmlEncode(); + var autoreplyText, keywords; + autoreplyText = $('#messageText').val().htmlEncode(); if(!(isGroupChecked("blankKeyword"))){ - var keywords = $('#keywords').val().toUpperCase(); + keywords = $('#keywords').val().toUpperCase(); $("#keyword-confirm").html('

    ' + keywords + '

    '); } else { $("#keyword-confirm").html('

    ' + i18n("autoreply.blank.keyword") + '

    '); diff --git a/plugins/frontlinesms-core/grails-app/views/connection/_confirm.gsp b/plugins/frontlinesms-core/grails-app/views/connection/_confirm.gsp index 0e7446bc7..dd92f4a9a 100644 --- a/plugins/frontlinesms-core/grails-app/views/connection/_confirm.gsp +++ b/plugins/frontlinesms-core/grails-app/views/connection/_confirm.gsp @@ -2,7 +2,8 @@

    - +
    + diff --git a/plugins/frontlinesms-core/grails-app/views/connection/_connection.gsp b/plugins/frontlinesms-core/grails-app/views/connection/_connection.gsp new file mode 100644 index 000000000..da8716cfd --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/connection/_connection.gsp @@ -0,0 +1,19 @@ +<%@ page import="frontlinesms2.api.FrontlineApi" %> + + + + + +

    + + +

    ${c.getFullApiUrl(request)}

    +
    + +

    ${c.displayMetadata}

    +
    +
    + +
    + + diff --git a/plugins/frontlinesms-core/grails-app/views/connection/_connection_list.gsp b/plugins/frontlinesms-core/grails-app/views/connection/_connection_list.gsp deleted file mode 100644 index 82fdf139d..000000000 --- a/plugins/frontlinesms-core/grails-app/views/connection/_connection_list.gsp +++ /dev/null @@ -1,59 +0,0 @@ -<%@ page import="frontlinesms2.ConnectionStatus" %> -
    -
    -

    -
      -
    • - - - -
    • -
    -
    -
    -
    - -

    -
    - -
      - -
    • - -
      -

      '${c.name}'

      -

      ()

      - -

      ${"http://you-ip-address"+createLink(uri: '/')+"api/1/smssync/"+c.id+"/"}

      -
      -

      -
      -
      - - -
      - - - - - - - - - - - - - - - - - -
      -
      -
    • -
      -
    -
    -
    - diff --git a/plugins/frontlinesms-core/grails-app/views/connection/_details.gsp b/plugins/frontlinesms-core/grails-app/views/connection/_details.gsp index bf9787d1b..318b0289a 100644 --- a/plugins/frontlinesms-core/grails-app/views/connection/_details.gsp +++ b/plugins/frontlinesms-core/grails-app/views/connection/_details.gsp @@ -1,15 +1,16 @@ <%@ page import="frontlinesms2.*" %>
    -

    - +

    +
    - +
    -

    - +

    +
    + diff --git a/plugins/frontlinesms-core/grails-app/views/connection/_frontlinesync.gsp b/plugins/frontlinesms-core/grails-app/views/connection/_frontlinesync.gsp new file mode 100644 index 000000000..aac89f3df --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/connection/_frontlinesync.gsp @@ -0,0 +1,67 @@ +<%@ page import="frontlinesms2.FrontlinesyncFconnection" %> +
    +

    + +
    +
    +
    +
    +
    +
    ${c.id}
    +
    +
    +
    +
    +
    +
    +
    ${c.secret}
    +
    +
    +
    +
    +
    +
    + +
    + + + + ${message(code:"frontlinesync.sync.config.dirty."+(!c.configSynced))} + +
    + +
    + +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + +if(typeof frontlineSyncCheckSettingOptions == 'undefined') { + frontlineSyncCheckSettingOptions = []; + + frontlineSyncCheckSettingOptions.push({ + 'valueInMinutes': ${minuteVal}, + 'i18n': "${g.message(code: 'frontlinesync.checkInterval.' + (minuteVal ?: 'manual'))}" + }); + +} + + diff --git a/plugins/frontlinesms-core/grails-app/views/connection/_routing.gsp b/plugins/frontlinesms-core/grails-app/views/connection/_routing.gsp new file mode 100644 index 000000000..091e58418 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/connection/_routing.gsp @@ -0,0 +1,34 @@ +
    +

    + +
    + + + + + +
  • + +
  • +
    +
    +
    + + + +
  • + +
  • +
    +
    +
    +
    +
    diff --git a/plugins/frontlinesms-core/grails-app/views/connection/_smssync.gsp b/plugins/frontlinesms-core/grails-app/views/connection/_smssync.gsp new file mode 100644 index 000000000..c89633de6 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/connection/_smssync.gsp @@ -0,0 +1,2 @@ +

    ${c.getFullApiUrl(request)} | +${g.message(code:"smssync.lastConnected.${c.lastConnectionTime ? 'time' : 'never'}", args:[c.lastConnectionTime ? g.formatDate(date:c.lastConnectionTime):null])} diff --git a/plugins/frontlinesms-core/grails-app/views/connection/_type.gsp b/plugins/frontlinesms-core/grails-app/views/connection/_type.gsp index 8960c25fb..d52244104 100644 --- a/plugins/frontlinesms-core/grails-app/views/connection/_type.gsp +++ b/plugins/frontlinesms-core/grails-app/views/connection/_type.gsp @@ -1,18 +1,11 @@ <%@ page import="frontlinesms2.*" %> -

    - -
      - -
    • - - -

      -
    • -
      -
    -
    + diff --git a/plugins/frontlinesms-core/grails-app/views/connection/createTest.gsp b/plugins/frontlinesms-core/grails-app/views/connection/createTest.gsp index 1b0c85bc1..8a368d876 100644 --- a/plugins/frontlinesms-core/grails-app/views/connection/createTest.gsp +++ b/plugins/frontlinesms-core/grails-app/views/connection/createTest.gsp @@ -1,22 +1,29 @@ <%@ page contentType="text/html;charset=UTF-8" %> - - - - - - - - - - - -
    - - - -
    - - - -
    -
    + + + + + + + + + + + + + + + + +
    + + + +
    + + + +
    +
    + + diff --git a/plugins/frontlinesms-core/grails-app/views/connection/list.gsp b/plugins/frontlinesms-core/grails-app/views/connection/list.gsp new file mode 100644 index 000000000..27c3e429a --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/connection/list.gsp @@ -0,0 +1,112 @@ +<%@ page import="frontlinesms2.ConnectionStatus" %> + + + + <g:message code="connection.header"/> ${connectionInstance?.name} + + +
    +
    +

    +
      +
    • + + + +
    • +
    +
    +
    +
    + +

    +
    + + + + + +
    +
    + +
    + + + + + + + + + + + + + +$(function() { + + + fconnection_list.update("${c.status}", ${c.id}); + + + app_info.listen("fconnection_statuses", function(data) { + var i; + data = data.fconnection_statuses; + if(!data) { return; } + for(i=data.length-1; i>=0; --i) { + if(data[i].userMutable) { + fconnection_list.update(data[i].status, data[i].id); + } + } + }); + + app_info.listen("frontlinesync_config_synced_status", function(data){ + var i, data = data.frontlinesync_config_synced_status; + for(i=data.length-1; i>=0; --i) { + frontlinesync.updateConfigSynced(data[i]); + } + }); + preloadImage("${r.resource(dir:'images', file:'message/gray-ajax-spinner.gif')}"); + + fconnection_list.pulseNewConnections("${newConnectionIds}"); + +}); + + + + diff --git a/plugins/frontlinesms-core/grails-app/views/connection/show.gsp b/plugins/frontlinesms-core/grails-app/views/connection/show.gsp deleted file mode 100644 index 822806fda..000000000 --- a/plugins/frontlinesms-core/grails-app/views/connection/show.gsp +++ /dev/null @@ -1,31 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" %> - - - - <g:message code="connection.header"/> ${connectionInstance?.name} - - - $(function() { - var count = 0; - var oldSystemNotificationCount = $("div.system-notification").length; - var connectionTimer = setInterval(refreshConnectionStatus, 2000); - function refreshConnectionStatus() { - $.get("${createLink(controller:'connection', action:'list', id:params?.id)}", function(data) { - var newSystemNotificationCount = $("div.system-notification").length; - if (count < 2 && oldSystemNotificationCount == newSystemNotificationCount) { - count++; - } else { - clearInterval(connectionTimer); - $("div.flash").hide(); - $(".connections").replaceWith($(data).find('.connections')); - } - }); - } - }); - - - - - - - diff --git a/plugins/frontlinesms-core/grails-app/views/connection/wizard.gsp b/plugins/frontlinesms-core/grails-app/views/connection/wizard.gsp index 28628e637..0eac3959d 100644 --- a/plugins/frontlinesms-core/grails-app/views/connection/wizard.gsp +++ b/plugins/frontlinesms-core/grails-app/views/connection/wizard.gsp @@ -9,78 +9,39 @@
  • - + + + +
    +fconnection.getType = function() { + return "${fconnectionInstance?.shortName}"; + return $("input[name=connectionType]:checked").val(); +}; -var fconnection = { - getType: function() { - return "${fconnectionInstance?.shortName}"; - return $("input[name=connectionType]:checked").val(); - }, - setType: function(connectionType) { - if(!$("input[name=connectionType]:checked")) { - $("input[name=connectionType]").each(function() { - var e = $(this); - e.attr("checked", e.val() == connectionType); - }); - } - - $("#${it}-form").hide(); - - $("#" + connectionType + "-form").css('display', 'inline'); - fconnection.init(); - connectionTooltips.init(connectionType); - }, - init: function() { - var keys = fconnection[fconnection.getType()].validationSubsectionFieldKeys; - $.each(keys, function(index, value) { - fconnection.toggleSubFields(value); +fconnection.setType = function(connectionType) { + if(!$("input[name=connectionType]:checked").size()) { + $("input[name=connectionType]").each(function() { + var e = $(this); + if (e.val() == connectionType) { + e.attr("checked", "checked"); + } }); - connectionTooltips.init(fconnection.getType()); - }, - show: function() { - setConfirmVal('type', fconnection.humanReadableName()); - setConfirmation('name'); - fconnection[fconnection.getType()].show(); - }, - isValid: function() { - var valid = true; - var keys = fconnection[fconnection.getType()].validationSubsectionFieldKeys; - if(keys.length > 1) { - valid = validateSections(keys); - if(!valid) return valid; - $.each(keys, function(index, value) { - valid = valid && isFieldValid(value); - return valid; - }); - } else { - var fields = fconnection[fconnection.getType()].requiredFields; - $.each(fields, function(index, value) { - valid = valid && isFieldValid(value); - return valid; - }); - } - return valid; - }, - humanReadableName: function() { - return fconnection[fconnection.getType()].humanReadableName; - }, - toggleSubFields: function(key) { - if(isSubsection(key)) { - // TODO why can't these be combined? - if(!getFieldVal(key)) - disableSubsectionFields(key); - if(getFieldVal(key)) - enableSubsectionFields(key); - } - }, + } + + $("#${it}-form").hide(); + + $("#" + connectionType + "-form").css('display', 'inline'); + fconnection.init(); + connectionTooltips.init(connectionType); +}; - - ${imp.shortName}: { + + fconnection.${imp.shortName} = { <% def asJs = { it? '"' + it.join('", "') + '"': '' } def nonNullableConfigFields = asJs(Fconnection.getNonnullableConfigFields(imp)) @@ -88,9 +49,9 @@ var fconnection = { %> requiredFields: [${nonNullableConfigFields}], validationSubsectionFieldKeys: [${validationSubsectionFieldKeys}], - humanReadableName: "", + humanReadableName: "", show: function() { - + $("#${it}-confirm").hide(); var validationSubsectionFieldKeys = fconnection[fconnection.getType()].validationSubsectionFieldKeys; @@ -106,135 +67,21 @@ var fconnection = { $("#${imp.shortName}-confirm").show(); } - }, - - _terminator: null // this is here to prevent the trailing comma which kills IE7 -}; - -function isFieldValid(fieldName) { - var val = getFieldVal(fieldName); - if(typeof(val) === "boolean") { - if(val && isSubsection(fieldName)) { - return validateSubsectionFields(fieldName); - } - } else { - return !getField(fieldName).hasClass("error") && isFieldSet(fieldName); - } -} - -function isFieldSet(fieldName) { - var val = getFieldVal(fieldName); - return val!==null && val.length>0; -} - -function validateSubsectionFields(field) { - var valid = false; - var subSectionFields = $('.' + field + '-subsection-member'); - var requiredFields = fconnection[fconnection.getType()].requiredFields; - $.each(subSectionFields, function(index, value) { - var field = $(value).attr("field"); - if(requiredFields.indexOf(field) > -1) { - valid = isFieldValid(field); - return valid; - } - }); - return valid; -} - -function validateSections(keys) { - var valid = false; - $.each(keys, function(index, value) { - if(isSubsection(value)) { - valid = getFieldVal(value); - if(valid) return false; - } - }); - return valid; -} - -function isSubsection(fieldName) { - return $('#' + fieldName + '-subsection').length > 0; -} - -function disableSubsectionFields(field) { - var subSectionFields = $('.' + field + '-subsection-member'); - $.each(subSectionFields, function(index, value) { - $(value).disableField(); - }); -} - -function enableSubsectionFields(field) { - var subSectionFields = $('.' + field + '-subsection-member'); - $.each(subSectionFields, function(index, value) { - $(value).enableField(); - }); -} - -function getFieldVal(fieldName) { - var field = getField(fieldName); - if(field.attr("type") === "checkbox") { - return field.prop("checked"); - } else { - return field.val(); - } -} - -function getField(fieldName) { - return $('#' + fconnection.getType() + fieldName); -} - -function setConfirmVal(fieldName, val) { - var isCheckbox = $('#' + fconnection.getType() + fieldName).attr("type") === "checkbox"; - - if(isCheckbox == true) { - var text = (val == true) ? "Yes": "No"; - $("#" + fconnection.getType() + "-confirm #confirm-" + fieldName).text(text); - } else if($('#' + fconnection.getType() + fieldName).is(":disabled") === false) { - $("#" + fconnection.getType() + "-confirm #confirm-" + fieldName).parent().removeClass("hide"); - $("#" + fconnection.getType() + "-confirm #confirm-" + fieldName).text(val); - } else { - $("#" + fconnection.getType() + "-confirm #confirm-" + fieldName).parent().addClass("hide"); - } -} - -function setConfirmation(fieldName) { - setConfirmVal(fieldName, getFieldVal(fieldName)); -} - -function setSecretConfirmation(fieldName) { - var val = isFieldSet(fieldName)? '****': 'None'; // FIXME i18n - setConfirmVal(fieldName, val); -} - -function attachCheckBoxListener() { - $("input[type='checkbox']").bind("change", function() { - var key = $(this).attr("field"); - fconnection.toggleSubFields(key); - }); -} + }; + function initializePopup() { - $("#connectionForm").validate(); + fconnection.validator(); - fconnection.setType("${fconnectionInstance?fconnectionInstance.getClass().shortName: 'smslib'}"); + fconnection.setType("${fconnectionInstance?fconnectionInstance.getClass().shortName: Fconnection.implementations[0].shortName}"); fconnection.init(); $("#tabs").bind("tabsshow", fconnection.show); - attachCheckBoxListener(); + fconnection.attachCheckBoxListener(); $("#tabs-2").contentWidget({ validate: fconnection.isValid }); } - -function handleSaveResponse(response) { - if(response.ok) { - window.location = response.redirectUrl; - } else { - var errors = $(".error-panel"); - errors.text(response.text); - errors.show(); - $("#submit").removeAttr('disabled'); - } -} + diff --git a/plugins/frontlinesms-core/grails-app/views/contact/_contact_list.gsp b/plugins/frontlinesms-core/grails-app/views/contact/_contact_list.gsp index 8b8a13e7d..ced0a91ca 100644 --- a/plugins/frontlinesms-core/grails-app/views/contact/_contact_list.gsp +++ b/plugins/frontlinesms-core/grails-app/views/contact/_contact_list.gsp @@ -1,39 +1,43 @@ <%@ page contentType="text/html;charset=UTF-8" %> - -
      - -
    • - - -
    • -
      - -
    • - - -
    • -
      - -
    • - - - - - - - - - - ${c.name?:c.mobile?:'[No Name]'} - + +
        + +
      • + + +
      • +
        + +
      • + + +
      • +
        + +
      • + + + + + + + + + + ${c.name} + + ${c.mobile?.toPrettyPhoneNumber()?:'-'} + + + +
      • +
        +
      +
      + +
      +

      +
      +
      -
    • -
      -
    -
    - -
    -

    -
    -
    diff --git a/plugins/frontlinesms-core/grails-app/views/contact/_custom_field.gsp b/plugins/frontlinesms-core/grails-app/views/contact/_custom_field.gsp new file mode 100644 index 000000000..6f1110639 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/contact/_custom_field.gsp @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/plugins/frontlinesms-core/grails-app/views/contact/_group_membership.gsp b/plugins/frontlinesms-core/grails-app/views/contact/_group_membership.gsp new file mode 100644 index 000000000..0d1f57cb3 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/contact/_group_membership.gsp @@ -0,0 +1,4 @@ +
  • + ${name} + +
  • diff --git a/plugins/frontlinesms-core/grails-app/views/contact/_header.gsp b/plugins/frontlinesms-core/grails-app/views/contact/_header.gsp index 12036719b..f567518a5 100644 --- a/plugins/frontlinesms-core/grails-app/views/contact/_header.gsp +++ b/plugins/frontlinesms-core/grails-app/views/contact/_header.gsp @@ -1,25 +1,23 @@
    -

    ${contactsSection.name} (${contactInstanceTotal})

    +

    ${contactsSection.name} (${contactsSectionContactTotal})

    + noSelection="${['': g.message(code:'group.moreactions')]}" + onchange="selectmenuTools.snapback(this)"/> + noSelection="${['': g.message(code:'group.moreactions')]}" + onchange="selectmenuTools.snapback(this)"/> -

    -
    - -
    diff --git a/plugins/frontlinesms-core/grails-app/views/contact/_import_contacts.gsp b/plugins/frontlinesms-core/grails-app/views/contact/_import_contacts.gsp new file mode 100644 index 000000000..25ababa8e --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/contact/_import_contacts.gsp @@ -0,0 +1,9 @@ + + + + + + diff --git a/plugins/frontlinesms-core/grails-app/views/contact/_menu.gsp b/plugins/frontlinesms-core/grails-app/views/contact/_menu.gsp index df487c770..0842ef66e 100644 --- a/plugins/frontlinesms-core/grails-app/views/contact/_menu.gsp +++ b/plugins/frontlinesms-core/grails-app/views/contact/_menu.gsp @@ -7,6 +7,17 @@ + + + + + + + + + + + @@ -14,9 +25,9 @@ - + - + @@ -24,14 +35,14 @@ - + - + - + var createSmartGroup = function() { $("#submit").attr('disabled', 'disabled'); if(validateSmartGroup()) { @@ -42,4 +53,4 @@ $('.error-panel').show(); } }; - + diff --git a/plugins/frontlinesms-core/grails-app/views/contact/_multiple_contact_view.gsp b/plugins/frontlinesms-core/grails-app/views/contact/_multiple_contact.gsp similarity index 84% rename from plugins/frontlinesms-core/grails-app/views/contact/_multiple_contact_view.gsp rename to plugins/frontlinesms-core/grails-app/views/contact/_multiple_contact.gsp index 2a10989c1..a126176ea 100644 --- a/plugins/frontlinesms-core/grails-app/views/contact/_multiple_contact_view.gsp +++ b/plugins/frontlinesms-core/grails-app/views/contact/_multiple_contact.gsp @@ -8,7 +8,7 @@
  • ${g.name} - +
  • @@ -30,8 +30,8 @@
    - + - +
    diff --git a/plugins/frontlinesms-core/grails-app/views/contact/_single_contact.gsp b/plugins/frontlinesms-core/grails-app/views/contact/_single_contact.gsp new file mode 100644 index 000000000..0b81a3ee3 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/contact/_single_contact.gsp @@ -0,0 +1,158 @@ +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    + + + + + +
    +
    + +
    + + +
    +
      + + + +
    • +

      +
    • +
    +
    + +
    + + +
    +

    +
      +
    • +
    • +
    + + + + + + + +
    +
    +
    + +$(function() { + app_info.listen("contact_message_stats", { id: "${contactInstance?.id}" }, function(data) { + data = data.contact_message_stats; + if(!data) { return; } + $("#contact-infos .sent").text(i18n("contact.messages.sent", data.outbound)); + $("#contact-infos .recieved").text(i18n("contact.messages.received", data.inbound)); + }); + + $("#single-contact").find("input,a.stroked,textarea").each(function(i, e) { + e = $(e); + if(!e.is(":visible")) { + return; + } + $(e).attr("tabindex", i); + }); + + $(document.documentElement).keyup(function(event) { + var key; + var saveAnchor = $("#single-contact a.save"); + var saveInput = $("#single-contact input.save"); + if(event) { + key = event.which; + } else { + key = event.keyCode; + } + if((key == 13) && (saveAnchor.is(":focus"))) { + saveInput.trigger("click"); + } + }); +}); + + + + + diff --git a/plugins/frontlinesms-core/grails-app/views/contact/_single_contact_view.gsp b/plugins/frontlinesms-core/grails-app/views/contact/_single_contact_view.gsp deleted file mode 100644 index 3d8f89d11..000000000 --- a/plugins/frontlinesms-core/grails-app/views/contact/_single_contact_view.gsp +++ /dev/null @@ -1,138 +0,0 @@ -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - - - - -   - - - -
    -
    - -   -
    - -
    -
      - -
    • - ${g.name} -
    • -
      -
    • -

      -
    • -
    -
    - -
    -
    - - - - - - - - - - - - - - -
    - -
    -

    -
      -
    • -
    • -
    - - - -
    -
    -
    - -function refreshMessageStats(data) { - var url = 'contact/messageStats'; - var numSent = $('#num-sent'); - var numRecieved = $('#num-recieved'); - $.getJSON(url_root + url, {id: "${contactInstance?.id}"},function(data) { - numSent.text(numSent.text().replace(/\d{1,}/, data.outboundMessagesCount)); - numRecieved.text(numRecieved.text().replace(/\d{1,}/, data.inboundMessagesCount)); - }); -} - -$(function() { - setInterval(refreshMessageStats, 15000); -}); - - diff --git a/plugins/frontlinesms-core/grails-app/views/contact/newCustomField.gsp b/plugins/frontlinesms-core/grails-app/views/contact/newCustomField.gsp index 799a438ae..e409932ff 100644 --- a/plugins/frontlinesms-core/grails-app/views/contact/newCustomField.gsp +++ b/plugins/frontlinesms-core/grails-app/views/contact/newCustomField.gsp @@ -1,6 +1,6 @@ <%@ page contentType="text/html;charset=UTF-8" %>
    - + diff --git a/plugins/frontlinesms-core/grails-app/views/contact/show.gsp b/plugins/frontlinesms-core/grails-app/views/contact/show.gsp index 9019ddf32..7a7cd4507 100644 --- a/plugins/frontlinesms-core/grails-app/views/contact/show.gsp +++ b/plugins/frontlinesms-core/grails-app/views/contact/show.gsp @@ -1,7 +1,14 @@ <%@ page import="frontlinesms2.Contact" %> - ${contactsSection ? "Contacts >> ${contactsSection.name}" : 'Contacts'} + + <g:if test="${contactsSection == null}"> + <g:message code="contact.header"/> + </g:if> + <g:else> + <g:message code="contact.header.group" args="[contactsSection.name]"/> + </g:else> + $(function() { @@ -13,10 +20,14 @@
    - - + + - + +

    +
    +
    + diff --git a/plugins/frontlinesms-core/grails-app/views/customactivity/_config.gsp b/plugins/frontlinesms-core/grails-app/views/customactivity/_config.gsp new file mode 100644 index 000000000..70a31f2f7 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/customactivity/_config.gsp @@ -0,0 +1,24 @@ +
    +

    +
    + +
    + +
    + + +
      + + + + + +
    + diff --git a/plugins/frontlinesms-core/grails-app/views/customactivity/_confirm.gsp b/plugins/frontlinesms-core/grails-app/views/customactivity/_confirm.gsp new file mode 100644 index 000000000..a6dde2de8 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/customactivity/_confirm.gsp @@ -0,0 +1,18 @@ +
    + + +
    +
    +

    +
    + + + + + + +
    +
    +
    +
    + diff --git a/plugins/frontlinesms-core/grails-app/views/customactivity/_save.gsp b/plugins/frontlinesms-core/grails-app/views/customactivity/_save.gsp new file mode 100644 index 000000000..13be3f1d8 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/customactivity/_save.gsp @@ -0,0 +1,6 @@ +
    + +

    +

    +

    +
    diff --git a/plugins/frontlinesms-core/grails-app/views/customactivity/_step.gsp b/plugins/frontlinesms-core/grails-app/views/customactivity/_step.gsp new file mode 100644 index 000000000..b8e1266ff --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/customactivity/_step.gsp @@ -0,0 +1,10 @@ +
  • + × + +

    +
    + + + ${body()} +
  • + diff --git a/plugins/frontlinesms-core/grails-app/views/customactivity/_validate.gsp b/plugins/frontlinesms-core/grails-app/views/customactivity/_validate.gsp new file mode 100644 index 000000000..64ff1efd1 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/customactivity/_validate.gsp @@ -0,0 +1,11 @@ + + function initializePopup() { + custom_activity.steps = ["join", "leave", "reply", "webconnection"]; + custom_activity.init(); + customActivityDialog.addValidationRules(); + + var initialScripts = ; + webconnectionDialog.setScripts(initialScripts); + } + + diff --git a/plugins/frontlinesms-core/grails-app/views/customactivity/create.gsp b/plugins/frontlinesms-core/grails-app/views/customactivity/create.gsp new file mode 100644 index 000000000..bd3387d6b --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/customactivity/create.gsp @@ -0,0 +1,11 @@ + + + diff --git a/plugins/frontlinesms-core/grails-app/views/customactivity/steps/_forward.gsp b/plugins/frontlinesms-core/grails-app/views/customactivity/steps/_forward.gsp new file mode 100644 index 000000000..9c8a8f86f --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/customactivity/steps/_forward.gsp @@ -0,0 +1,6 @@ + +
    + + +
    +
    diff --git a/plugins/frontlinesms-core/grails-app/views/customactivity/steps/_join.gsp b/plugins/frontlinesms-core/grails-app/views/customactivity/steps/_join.gsp new file mode 100644 index 000000000..99c9e6f85 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/customactivity/steps/_join.gsp @@ -0,0 +1,10 @@ +<%@ page import="frontlinesms2.Group" %> + + + + diff --git a/plugins/frontlinesms-core/grails-app/views/customactivity/steps/_leave.gsp b/plugins/frontlinesms-core/grails-app/views/customactivity/steps/_leave.gsp new file mode 100644 index 000000000..14fa4ea6e --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/customactivity/steps/_leave.gsp @@ -0,0 +1,10 @@ +<%@ page import="frontlinesms2.Group" %> + + + + diff --git a/plugins/frontlinesms-core/grails-app/views/customactivity/steps/_reply.gsp b/plugins/frontlinesms-core/grails-app/views/customactivity/steps/_reply.gsp new file mode 100644 index 000000000..6cf6c8a6c --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/customactivity/steps/_reply.gsp @@ -0,0 +1,5 @@ + +
    + +
    +
    diff --git a/plugins/frontlinesms-core/grails-app/views/customactivity/steps/_webconnectionStep.gsp b/plugins/frontlinesms-core/grails-app/views/customactivity/steps/_webconnectionStep.gsp new file mode 100644 index 000000000..8a14c5edc --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/customactivity/steps/_webconnectionStep.gsp @@ -0,0 +1,50 @@ +<%@ page import="frontlinesms2.WebconnectionActionStep" %> + +
    + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + +
    + + + +
    + + + +
    diff --git a/plugins/frontlinesms-core/grails-app/views/customactivity/steps/_webconnectionStepParameter.gsp b/plugins/frontlinesms-core/grails-app/views/customactivity/steps/_webconnectionStepParameter.gsp new file mode 100644 index 000000000..eccb6292a --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/customactivity/steps/_webconnectionStepParameter.gsp @@ -0,0 +1,16 @@ +<%@ page import="frontlinesms2.Webconnection" %> + + + + + + + + + + + + - + + + diff --git a/plugins/frontlinesms-core/grails-app/views/export/contactWizard.gsp b/plugins/frontlinesms-core/grails-app/views/export/contactWizard.gsp index 58bf743ff..4765ad7e6 100644 --- a/plugins/frontlinesms-core/grails-app/views/export/contactWizard.gsp +++ b/plugins/frontlinesms-core/grails-app/views/export/contactWizard.gsp @@ -7,8 +7,18 @@

    -
    - + + +
    diff --git a/plugins/frontlinesms-core/grails-app/views/export/messageWizard.gsp b/plugins/frontlinesms-core/grails-app/views/export/messageWizard.gsp index aa63ea5a0..dd1305250 100644 --- a/plugins/frontlinesms-core/grails-app/views/export/messageWizard.gsp +++ b/plugins/frontlinesms-core/grails-app/views/export/messageWizard.gsp @@ -14,8 +14,3 @@ - -function updateExportInfo() { - $(".ui-dialog-title").html("Export Messages: ${reportName}"); -} - diff --git a/plugins/frontlinesms-core/grails-app/views/fconnection/frontlinesync/_config.gsp b/plugins/frontlinesms-core/grails-app/views/fconnection/frontlinesync/_config.gsp new file mode 100644 index 000000000..96d799485 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/fconnection/frontlinesync/_config.gsp @@ -0,0 +1,25 @@ +
    +

    +
    +

    +
    +
    + + <% + def connectionsCount = frontlinesms2.FrontlinesyncFconnection.countByNameLike("%FrontlineSync%") + def suffix = connectionsCount?" ($connectionsCount)" :'' + def connectionName = fconnectionInstance?.name?:'FrontlineSync'+ suffix + %> + +
    + +
    + + + +
    + + Android app on Google Play + + +
    diff --git a/plugins/frontlinesms-core/grails-app/views/help/_index.gsp b/plugins/frontlinesms-core/grails-app/views/help/_index.gsp index 58e3da4bc..47b7c1ec5 100644 --- a/plugins/frontlinesms-core/grails-app/views/help/_index.gsp +++ b/plugins/frontlinesms-core/grails-app/views/help/_index.gsp @@ -25,6 +25,14 @@ * [How do I create an announcement][31] * ### Auto Reply * [How do I create an Auto-Reply?][32] + * ### Auto Forward + * [How do I create an Auto-Forward?][100] + * ### Subscription + * [How do I create an Subscription][101] + * ### Web Connection + * [How do I create an Web Connection?][102] + * ### Custom Activity + * [How do I create a Custom Activity][105] * ### Managing your Activities * [How do I Archive an Activity?][34] * [How do I Export an Activity?][35] @@ -52,6 +60,7 @@ * [How do I create a Group?][60] * ### Managing your Contacts * [How do I see the messages that have been sent and received by a particular contact?][62] + * [How do I import contacts?](settings/11.contact_import) * [How do I export contacts?][63] * [How do I search through the contacts list?][64] * ### Advanced @@ -62,77 +71,89 @@ * [How do I perform a simple search?][72] * ### Advanced * [How do I perform a much more detailed search?][74] -* ### Status - * [What is the Status Tab?][82] - * [How do I see how many messages I have sent or received?][87] -* ### Settings - * [How do I change the Language?][91] - * [How do I import Messages and Contacts?][92] +* ### Connections + * [How do I set up FrontlineSync to work with FrontlineSMS?][106] + * [How do I locate my FrontlineSMS workspace so that I can connect it to FrontlineSync?][107] * [How do I use my modem/phone with FrontlineSMS?][94] - * [How do I set up an IntelliSMS account?][95] - * [How do I set up a Clickatell account?][96] + * [How do I use my Android phone/SMSSync with FrontlineSMS?][95] + * [How do I connect FrontlineSMS to a Short Message Service Center (SMSC)?][96] * [How do I edit/delete a Connection?][97] * [How do I get FrontlineSMS to detect my device?][98] +* ### Settings + * [How do I change the Language?][91] + * [How do I import Messages and Contacts?][92] + * [What are Usage Statistics?][82] + * [How do I see how many messages I have sent or received?][87] + * [How do I password-protect my FrontlineSMS installation?][103] * [How do I translate the interface into my language?][99] + * [How do I create rules for sending messages through multiple Connections][104] -[1]: core/features/new -[2]: core/messages/1.getting_around_the_messages_tab -[5]: core/messages/2.sss -[6]: core/messages/3.quick_message -[7]: core/messages/8.mrfd -[9]: core/archive/2.archiving_messages -[12]: core/messages/5.sent -[13]: core/messages/6.trash -[14]: core/messages/7.pending -[16]: core/messages/2.sss -[18]: core/messages/4.filtering_messages -[19]: core/messages/7.pending -[20]: core/messages/2.sss -[22]: core/activities/1.getting_around_activities -[23]: core/activities/2.creating_an_activity -[24]: core/activities/1.getting_around_activities -[26]: core/activities/3.creating_a_poll -[30]: core/activities/3a.manually_categorising -[31]: core/activities/4.creating_an_announcement -[32]: core/activities/5.creating_an_auto-reply -[34]: core/archive/3.archiving_activities_folders -[35]: core/messages/9.exporting -[36]: core/activities/6.renaming_an_activity -[37]: core/messages/7.pending -[39]: core/folders/1.getting_around_folders -[40]: core/folders/2.creating_a_folder -[41]: core/messages/8.mrfd -[43]: core/archive/1.getting_around_the_archive_tab -[45]: core/archive/1a.inbox_archive -[46]: core/archive/1b.sent_archive -[47]: core/archive/1c.activity_archive -[48]: core/archive/1d.folder_archive -[50]: core/archive/2.archiving_messages -[51]: core/archive/3.archiving_activities_folders -[53]: core/messages/8.mrfd -[54]: core/archive/4.unarchive -[56]: core/contacts/1.getting_around_the_contacts_tab -[58]: core/contacts/2.add_contact -[59]: core/contacts/2a.editing_a_contact -[60]: core/contacts/4.creating_a_group -[62]: core/contacts/7.messages_sent_or_received_by_contact -[63]: core/messages/9.exporting -[64]: core/contacts/8.searching_through_contacts -[66]: core/contacts/3.add_remove_a_custom_field -[67]: core/contacts/6.add_remove_contact_to_from_a_group -[70]: core/search/1.getting_around_the_search_tab -[72]: core/search/2.creating_a_search -[74]: core/search/2a.creating_an_advanced_search -[80]: core/messages/9.exporting -[82]: core/status/1.getting_around_the_status_tab -[87]: core/status/2.using_the_traffic_graph -[90]: core/settings/1.getting_around_the_settings_menu -[91]: core/settings/2.changing_languages -[92]: core/settings/3.restoring_a_backup -[94]: core/settings/4.setting_up_a_device -[95]: core/settings/4a.intellisms -[96]: core/settings/4b.clickatell -[97]: core/settings/6.edit_delete_connection -[98]: core/settings/5.manually_adding_device -[99]: core/settings/7.translatingfrontlinesms +[1]: features/new +[2]: messages/1.getting_around_the_messages_tab +[5]: messages/2.sss +[6]: messages/3.quick_message +[7]: messages/8.mrfd +[9]: archive/2.archiving_messages +[12]: messages/5.sent +[13]: messages/6.trash +[14]: messages/7.pending +[16]: messages/2.sss +[18]: messages/4.filtering_messages +[19]: messages/7.pending +[20]: messages/2.sss +[22]: activities/1.getting_around_activities +[23]: activities/2.creating_an_activity +[24]: activities/1.getting_around_activities +[26]: activities/3.creating_a_poll +[30]: activities/3a.manually_categorising +[31]: activities/4.creating_an_announcement +[32]: activities/5.creating_an_auto-reply +[34]: archive/3.archiving_activities_folders +[35]: messages/9.exporting +[36]: activities/6.renaming_an_activity +[37]: messages/7.pending +[39]: folders/1.getting_around_folders +[40]: folders/2.creating_a_folder +[41]: messages/8.mrfd +[43]: archive/1.getting_around_the_archive_tab +[45]: archive/1a.inbox_archive +[46]: archive/1b.sent_archive +[47]: archive/1c.activity_archive +[48]: archive/1d.folder_archive +[50]: archive/2.archiving_messages +[51]: archive/3.archiving_activities_folders +[53]: messages/8.mrfd +[54]: archive/4.unarchive +[56]: contacts/1.getting_around_the_contacts_tab +[58]: contacts/2.add_contact +[59]: contacts/2a.editing_a_contact +[60]: contacts/4.creating_a_group +[62]: contacts/7.messages_sent_or_received_by_contact +[63]: messages/9.exporting +[64]: contacts/8.searching_through_contacts +[66]: contacts/3.add_remove_a_custom_field +[67]: contacts/6.add_remove_contact_to_from_a_group +[70]: search/1.getting_around_the_search_tab +[72]: search/2.creating_a_search +[74]: search/2a.creating_an_advanced_search +[80]: messages/9.exporting +[82]: status/1.getting_around_the_status_tab +[87]: status/2.using_the_traffic_graph +[90]: settings/1.getting_around_the_settings_menu +[91]: settings/2.changing_languages +[92]: settings/3.restoring_a_backup +[94]: settings/4.setting_up_a_device +[95]: settings/9.smssync +[96]: settings/smpp +[97]: settings/6.edit_delete_connection +[98]: settings/5.manually_adding_device +[99]: settings/7.translatingfrontlinesms +[100]: activities/8.creating_an_autoforward +[101]: activities/9.creating_a_subscription +[102]: activities/10.web_connection_api +[103]: settings/8.basic_auth +[104]: settings/10.custom_routing_rules +[105]: activities/11.custom_activity_builder +[106]: settings/4c.connecting_to_frontlinesync +[107]: settings/4d.finding_on_a_lan diff --git a/plugins/frontlinesms-core/grails-app/views/help/main.gsp b/plugins/frontlinesms-core/grails-app/views/help/index.gsp similarity index 68% rename from plugins/frontlinesms-core/grails-app/views/help/main.gsp rename to plugins/frontlinesms-core/grails-app/views/help/index.gsp index 10ea564c0..5fdcff3e9 100644 --- a/plugins/frontlinesms-core/grails-app/views/help/main.gsp +++ b/plugins/frontlinesms-core/grails-app/views/help/index.gsp @@ -1,25 +1,23 @@ function initializePopup() { - $("#modalBox.help #help-index li a").click(goToSection); + $("#modalBox.help #help-index li a").click(goToSectionFromIndex); $("#modalBox.help #help-content").delegate("a", "click", goToSection); $("div#help-index a:first").click(); - $.each($("#help-index > ul, #help-index li:has(ul)"), function(i, selecter) { + $.each($("#help-index > ul"), function(i, selecter) { $(selecter).accordion({ collapsible: true, heightStyle: "content", - autoHeight: false, - active: true + autoHeight: false, + active: true }); }); } -function goToSection() { - var menuItem = $(this); - +function _goToSection(menuItem, prefixTrimLength) { var section = menuItem.attr("href"); - if(section.indexOf("http:") == 0) { + if(section.indexOf("http:") === 0) { if(jQuery.browser.msie && jQuery.browser.version <= 7) { var loc = window.location.toString(); var lastSlash = loc.lastIndexOf("/") + 1; @@ -34,18 +32,27 @@ function goToSection() { } $("#modalBox.help #help-index li.selected").removeClass("selected"); menuItem.parent().addClass("selected"); - $("#help-content").load(url_root + "help/section", { helpSection:section }, function() { + $("#help-content").load(url_root + "help/" + section.substring(prefixTrimLength), function() { if(!jQuery.browser.msie || jQuery.browser.version > 7) { // This is a workaround for the image URL bug when viewing help from second+ // action URLs TODO long-term solution is to fix help generation/iframe it $("#help-content img").each(function(i, e) { e = $(e); - e.attr("src", url_root + "help/" + e.attr("src")); + e.attr("src", url_root + e.attr("src").substring(3)); }); } + $("#help-content").scrollTop(0); }); return false; } + +function goToSectionFromIndex() { + return _goToSection($(this), 0); +} + +function goToSection() { + return _goToSection($(this), 3); +}
    diff --git a/plugins/frontlinesms-core/grails-app/views/import/reviewContacts.gsp b/plugins/frontlinesms-core/grails-app/views/import/reviewContacts.gsp new file mode 100644 index 000000000..bbeb38886 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/import/reviewContacts.gsp @@ -0,0 +1,54 @@ +<%@ page contentType="text/html;charset=UTF-8" %> + + + + <g:message code="settings.import.contact.review.page.header"/> + + + +
    +

    +
    +
    +

    + +

    +

    + +

    + + + + + +

    +
    + + + + + + + + + + + + + +
    +
    + + + + +
    + + $(function() { + contactImportReviewer.init(); + $(".submitContacts").click(contactImportReviewer.submit); + }); + + + + diff --git a/plugins/frontlinesms-core/grails-app/views/message/_activity_buttons.gsp b/plugins/frontlinesms-core/grails-app/views/interaction/_activity_buttons.gsp similarity index 91% rename from plugins/frontlinesms-core/grails-app/views/message/_activity_buttons.gsp rename to plugins/frontlinesms-core/grails-app/views/interaction/_activity_buttons.gsp index 24cdc4fc2..3bfddc2cf 100644 --- a/plugins/frontlinesms-core/grails-app/views/message/_activity_buttons.gsp +++ b/plugins/frontlinesms-core/grails-app/views/interaction/_activity_buttons.gsp @@ -1,7 +1,7 @@ <%@ page import="frontlinesms2.*" %>
    - + @@ -13,7 +13,7 @@
    diff --git a/plugins/frontlinesms-core/grails-app/views/message/_footer.gsp b/plugins/frontlinesms-core/grails-app/views/interaction/_footer.gsp similarity index 90% rename from plugins/frontlinesms-core/grails-app/views/message/_footer.gsp rename to plugins/frontlinesms-core/grails-app/views/interaction/_footer.gsp index 1d3972b9f..bd6999c72 100644 --- a/plugins/frontlinesms-core/grails-app/views/message/_footer.gsp +++ b/plugins/frontlinesms-core/grails-app/views/interaction/_footer.gsp @@ -19,10 +19,10 @@
    - + + action="${messageSection}" total="${interactionInstanceTotal ?: itemInstanceTotal}" params="${params.findAll({it.key != 'interactionId'})}"/>
    diff --git a/plugins/frontlinesms-core/grails-app/views/message/_header.gsp b/plugins/frontlinesms-core/grails-app/views/interaction/_header.gsp similarity index 82% rename from plugins/frontlinesms-core/grails-app/views/message/_header.gsp rename to plugins/frontlinesms-core/grails-app/views/interaction/_header.gsp index 7ff7c075d..0a5322edf 100644 --- a/plugins/frontlinesms-core/grails-app/views/message/_header.gsp +++ b/plugins/frontlinesms-core/grails-app/views/interaction/_header.gsp @@ -6,13 +6,13 @@
    - +

    - +
    diff --git a/plugins/frontlinesms-core/grails-app/views/interaction/_interaction_details.gsp b/plugins/frontlinesms-core/grails-app/views/interaction/_interaction_details.gsp new file mode 100644 index 000000000..be8b2d802 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/interaction/_interaction_details.gsp @@ -0,0 +1,5 @@ +
    + + + +
    diff --git a/plugins/frontlinesms-core/grails-app/views/message/_message_list.gsp b/plugins/frontlinesms-core/grails-app/views/interaction/_interaction_list.gsp similarity index 64% rename from plugins/frontlinesms-core/grails-app/views/message/_message_list.gsp rename to plugins/frontlinesms-core/grails-app/views/interaction/_interaction_list.gsp index 0e73bd948..f2a7e4a6a 100644 --- a/plugins/frontlinesms-core/grails-app/views/message/_message_list.gsp +++ b/plugins/frontlinesms-core/grails-app/views/interaction/_interaction_list.gsp @@ -1,6 +1,6 @@ <%@page defaultCodec="html" %> - + @@ -10,7 +10,7 @@ - + @@ -21,24 +21,24 @@ - + - + - - - - + + + + - + - + ${m.displayName} @@ -51,14 +51,14 @@ - + ${m.messageOwner? m.messageOwner.getDisplayText(m).truncate(50) : m.text.truncate(50) } - - + + @@ -66,8 +66,8 @@ - - + + @@ -76,6 +76,9 @@ + + + diff --git a/plugins/frontlinesms-core/grails-app/views/interaction/_menu.gsp b/plugins/frontlinesms-core/grails-app/views/interaction/_menu.gsp new file mode 100644 index 000000000..e2bd009b3 --- /dev/null +++ b/plugins/frontlinesms-core/grails-app/views/interaction/_menu.gsp @@ -0,0 +1,49 @@ +<%@ page contentType="text/html;charset=UTF-8" import="frontlinesms2.TextMessage" %> +<%@ page contentType="text/html;charset=UTF-8" import="frontlinesms2.MissedCall" %> +<%@ page contentType="text/html;charset=UTF-8" import="frontlinesms2.FrontlinesyncFconnection" %> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/frontlinesms-core/grails-app/views/message/_message_actions.gsp b/plugins/frontlinesms-core/grails-app/views/interaction/_message_actions.gsp similarity index 65% rename from plugins/frontlinesms-core/grails-app/views/message/_message_actions.gsp rename to plugins/frontlinesms-core/grails-app/views/interaction/_message_actions.gsp index c2d009e25..31eeb8272 100644 --- a/plugins/frontlinesms-core/grails-app/views/message/_message_actions.gsp +++ b/plugins/frontlinesms-core/grails-app/views/interaction/_message_actions.gsp @@ -5,22 +5,21 @@ };
    -
    +
    - - + + + + - - - - - + + @@ -28,7 +27,7 @@ - + diff --git a/plugins/frontlinesms-core/grails-app/views/message/_move_message.gsp b/plugins/frontlinesms-core/grails-app/views/interaction/_move_message.gsp similarity index 100% rename from plugins/frontlinesms-core/grails-app/views/message/_move_message.gsp rename to plugins/frontlinesms-core/grails-app/views/interaction/_move_message.gsp diff --git a/plugins/frontlinesms-core/grails-app/views/message/_multiple_message_details.gsp b/plugins/frontlinesms-core/grails-app/views/interaction/_multiple_interaction_details.gsp similarity index 80% rename from plugins/frontlinesms-core/grails-app/views/message/_multiple_message_details.gsp rename to plugins/frontlinesms-core/grails-app/views/interaction/_multiple_interaction_details.gsp index 1083bbcb2..6cb7ff668 100644 --- a/plugins/frontlinesms-core/grails-app/views/message/_multiple_message_details.gsp +++ b/plugins/frontlinesms-core/grails-app/views/interaction/_multiple_interaction_details.gsp @@ -1,10 +1,10 @@ -