String definition / usage tests + cleaning of stale strings

This commit is contained in:
Alexandre Aubin 2020-04-04 01:14:18 +02:00
parent 8124d7e4f4
commit ffb3d4edde
8 changed files with 126 additions and 117 deletions

View file

@ -217,7 +217,7 @@
app.post('#/users/create', function (c) {
if (c.params['password'] == c.params['confirmation']) {
if (c.params['password'].length < PASSWORD_MIN_LENGTH) {
c.flash('fail', y18n.t('password_too_short'));
c.flash('fail', y18n.t('passwords_too_short'));
}
else {
// Force unit or disable quota
@ -380,7 +380,7 @@
if (params['password']) {
if (params['password'] == params['confirmation']) {
if (params['password'].length < PASSWORD_MIN_LENGTH) {
c.flash('fail', y18n.t('password_too_short'));
c.flash('fail', y18n.t('passwords_too_short'));
c.redirect_to('#/users/'+ c.params['user'] + '/edit', {slide: false});
}
else {

View file

@ -14,69 +14,6 @@
c.flash('warning', y18n.t('warning_first_user'));
}
/*
* Disabling this for now because there's a duplicate Access Allow
* Origin header thing preventing it from working and people keep
* complaining about it and we havent effectively used this in 2
* years anyway despite various security issues.
*
*
// Get security feed and display new items
var securityFeed = 'https://yunohost.org/security.rss';
var forumUrl = 'https://forum.yunohost.org';
$.ajax({
url: securityFeed,
// dataType: (jQuery.browser.msie) ? "text" : "xml",
dataType: "xml"
})
.done(function(xml){
// Get viewed security alerts from cookie
var viewedItems = Cookies.get('ynhSecurityViewedItems') || [];
// Get 6 month earlier date
var SixMonthEarlier = new Date();
SixMonthEarlier.setMonth(SixMonthEarlier.getMonth() - 6);
// Loop through items in a reverse order (older first)
$($('item', xml).get().reverse()).each(function(k, v) {
var link = $('link', v).text();
if (typeof link == 'string' && link !== '' && link.charAt(0) == '/') {
link = forumUrl+link;
}
// var description=$('description', v).text();
// description=description.replace('href="/','href="'+forumUrl+'/');
var item = {
guid: $('guid', v).text(),
title: $('title', v).text(),
url: link,
// desc: description,
date: new Date($('pubDate', v).text()),
};
// If item is not already viewed and is not older than 6 month
if (viewedItems.indexOf(item.guid) === -1 && (item.date.getTime() > SixMonthEarlier.getTime())) {
// Show security message to administrator
var warning = item.title + ' - ' +
item.date.toISOString().substring(0, 10) +
' (<a href="'+ item.url +'" class="alert-link" target="_blank">'+y18n.t('read_more')+'</a>)';
c.flash('warning', warning);
// Store viewed item
viewedItems.push(item.guid);
}
});
// Saved viewed items to cookie
Cookies.set('ynhSecurityViewedItems', viewedItems, {
expires: 7
});
})
.fail(function(stuff) {
c.flash('fail', y18n.t('error_retrieve_feed', [securityFeed]));
});
*/
c.api("GET", "/diagnosis/show?full", {}, function(data) {
c.hideLoader();
basesystem = data.reports.filter(function(r) { return r.id == "basesystem"; })[0];

View file

@ -3,7 +3,6 @@
"active": "Active",
"add": "Add",
"advanced": "Advanced",
"remove": "Remove",
"administration_password": "Administration password",
"all": "All",
"all_apps": "All apps",
@ -12,18 +11,14 @@
"app_change_url": "Change URL",
"app_info_access_desc": "Groups / users currently allowed to access this app:",
"app_info_changelabel_desc": "Change app label in the portal.",
"app_info_debug_desc": "Display debugging information for this application.",
"app_info_default_desc": "Redirect domain root to this application (%s).",
"app_info_changeurl_desc": "Change the access URL of this application (domain and/or path).",
"app_info_change_url_disabled_tooltip": "This feature hasn't been implemented in this app yet",
"app_info_uninstall_desc": "Remove this application.",
"app_install_cancel": "Installation cancelled.",
"app_install_custom_no_manifest": "No manifest.json file",
"app_level": "App level",
"app_make_default": "Make default",
"app_no_actions": "This application doesn't have any actions",
"app_repository": "Application origin: ",
"app_state": "Application state: ",
"app_state_inprogress": "not yet working",
"app_state_inprogress_explanation": "This maintainer of this app declared that this application is not ready yet for production use. BE CAREFUL!",
"app_state_notworking": "not working",
@ -34,22 +29,17 @@
"app_state_high-quality_explanation": "This app is well-integrated with YunoHost. It has been (and is!) peer-reviewed by the YunoHost app team. It can be expected to be safe and maintained on the long-term.",
"app_state_working": "working",
"app_state_working_explanation": "The maintainer of this app declared it as 'working'. It means that it should be functional (c.f. application level) but is not necessarily peer-reviewed, it may still contain issues or is not fully integrated with YunoHost.",
"application": "Application",
"applications": "Applications",
"archive_empty": "Empty archive",
"available_apps": "Available apps",
"backup": "Backup",
"backup_action": "Backup",
"backup_archive_copy": "Copy this archive on another storage",
"backup_archive_delete": "Delete this archive",
"backup_archive_download": "Download this archive",
"backup_content": "Backup content",
"backup_create": "Create a backup",
"backup_encryption_warning": "Don't forget this password, you'll need it if you want restore the archive",
"backup_new": "New backup",
"backup_optional_encryption": "Optional encryption",
"backup_optional_password": "Optional password",
"backup_type": "Type",
"backups_no": "No backup",
"begin": "Begin",
"both": "Both",
@ -84,9 +74,7 @@
"confirm_reboot_action_reboot": "Are you sure you want to reboot your server?",
"confirm_reboot_action_shutdown": "Are you sure you want to shutdown your server?",
"connection": "Connection",
"copy": "Copy",
"created_at": "Created at",
"current_maintainer_title": "Current maintainer of this package",
"custom_app_install": "Install custom app",
"custom_app_url_only_github": "Currently only from GitHub",
"default": "Default",
@ -107,26 +95,21 @@
"domain_add_dyndns_doc": "… and I want a dynamic DNS service.",
"domain_add_panel_with_domain": "I already have a domain name…",
"domain_add_panel_without_domain": "I don't have a domain name…",
"domain_default": "Default domain",
"domain_default_desc": "The default domain is the connection domain where users log in.",
"domain_default_longdesc": "This is your default domain.",
"domain_delete_longdesc": "Delete this domain",
"domain_dns_config": "DNS configuration",
"domain_dns_longdesc": "View DNS configuration",
"domain_list": "Domain list",
"domain_name": "Domain name",
"domain_select": "Select domain",
"domain_visit": "Visit",
"domain_visit_url": "Visit %s",
"domains": "Domains",
"download": "Download",
"enable": "Enable",
"enabled": "Enabled",
"errors": "%s errors",
"error_modify_something": "You should modify something",
"error_retrieve_feed": "Could not retrieve feed: %s. You might have a plugin prevent your browser from performing this request (or the website is down).",
"error_select_domain": "You should indicate a domain",
"error_server": "Server error",
"error_server_unexpected": "Unexpected server error (%s)",
"error_connection_interrupted": "The server closed the connection instead of answering it. Has nginx or the yunohost-api been restarted or stoppted for some reason? (Error code/message: %s)",
"everything_good": "Everything good!",
@ -134,7 +117,6 @@
"firewall": "Firewall",
"footer_version": "Powered by <a href='https://yunohost.org'>YunoHost</a> %s (%s).",
"form_input_example": "Example: %s",
"free": "Free",
"from_to": "from %s to %s",
"good_practices_about_admin_password": "You are now about to define a new admin password. The password should be at least 8 characters - though it is good practice to use longer password (i.e. a passphrase) and/or to use various kind of characters (uppercase, lowercase, digits and special characters).",
"good_practices_about_user_password": "You are now about to define a new user password. The password should be at least 8 characters - though it is good practice to use longer password (i.e. a passphrase) and/or to use various kind of characters (uppercase, lowercase, digits and special characters).",
@ -149,6 +131,7 @@
"group_explain_all_users": "This is a special group containing all users accounts on the server",
"group_explain_visitors": "This is a special group representing anonymous visitors",
"group_specific_permissions": "User specific permissions",
"groups": "Groups",
"groups_and_permissions": "Groups and permissions",
"groups_and_permissions_manage": "Manage groups and permissions",
"permissions": "Permissions",
@ -178,19 +161,15 @@
"install_time": "Install time",
"installation_complete": "Installation complete",
"installed": "Installed",
"installed_apps": "Installed apps",
"installing": "Installing",
"internal_exception": "<strong>Yunohost encountered an internal error:/</strong><br><em>Really sorry about that.<br>You should look for help on <a href=\"https://forum.yunohost.org/\">the forum</a> or <a href=\"https://chat.yunohost.org/\">the chat</a> to fix the situation, or report the bug on <a href=\"https://github.com/YunoHost/issues\">the bugtracker</a>.</em><br>The following information might be useful for the person helping you:<h3>Action</h3><pre>%s%s</pre><h3>Traceback</h3><pre>%s</pre>",
"ipv4": "IPv4",
"ipv6": "IPv6",
"label": "Label",
"label_for_manifestname": "Label for %s",
"last_ran": "Last time ran:",
"level": "level",
"license": "License",
"loading": "Loading …",
"local_archives": "Local archives",
"log": "Log",
"logged_in": "Logged in",
"logged_out": "Logged out",
"login": "Login",
@ -202,22 +181,18 @@
"manage_apps": "Manage apps",
"manage_domains": "Manage domains",
"manage_users": "Manage users",
"menu": "Menu",
"migrations": "Migrations",
"migrations_pending": "Pending migrations",
"migrations_done": "Previous migrations",
"migrations_no_pending": "No pending migrations",
"migrations_no_done": "No previous migrations",
"mode": "Mode",
"multi_instance": "Multi instance",
"myserver": "myserver",
"myserver_org": "myserver.org",
"next": "Next",
"no": "No",
"no_installed_apps": "No installed apps.",
"no_log": "No log.",
"nobody": "Nobody",
"non_compatible_api": "Non-compatible API",
"ok": "OK",
"only_highquality_apps": "Only high-quality apps",
"only_working_apps": "Only working apps",
@ -229,7 +204,6 @@
"others": "Others",
"password": "Password",
"password_confirmation": "Password confirmation",
"password_description": "Password must be at least %s characters long.",
"password_empty": "The password field is empty",
"password_new": "New password",
"passwords_dont_match": "Passwords don't match",
@ -275,12 +249,8 @@
"search_for_apps": "Search for apps...",
"select_all": "Select all",
"select_none": "Select none",
"service_description": "Description:",
"service_log": "%s log",
"service_start_on_boot": "Start on boot",
"service_status": "Status: ",
"services": "Services",
"services_list": "Service list",
"set_default": "Set default",
"size": "Size",
"since": "since",
@ -288,16 +258,10 @@
"start": "Start",
"status": "Status",
"stop": "Stop",
"storage_create": "Add remote storage",
"storages_new": "New remote storage",
"storages_no": "No storages.",
"system": "System",
"system_apps": "Apps",
"system_apps_nothing": "All apps are up to date!",
"system_packages": "System packages",
"system_packages_nothing": "All system packages are up to date!",
"system_update": "System update",
"system_upgrade": "System upgrade",
"system_upgrade_btn": "Upgrade",
"system_upgrade_all_applications_btn": "Upgrade all applications",
"system_upgrade_all_packages_btn": "Upgrade all packages",
@ -330,6 +294,7 @@
"unknown_action": "Unknown action %s",
"unknown_argument": "Unknown argument: %s",
"unmaintained": "Unmaintained",
"unmaintained_details": "This app has not been update for quite a while and the previous maintainer has gone away or does not have time to maintain this app. Feel free to check the app repository to provide your help",
"upload": "Upload",
"upload_archive": "Archive upload",
"upnp": "UPnP",
@ -348,7 +313,6 @@
"user_username": "Username",
"user_username_edit": "Edit %ss account",
"users": "Users",
"users_list": "User list",
"users_new": "New user",
"users_no": "No users.",
"version": "Version",
@ -364,7 +328,6 @@
"certificate_alert_great": "Great! You're using a valid Let's Encrypt certificate!",
"certificate_alert_unknown": "Unknown status",
"certificate_manage": "Manage SSL certificate",
"certificate_old_letsencrypt_app_conflict": "The 'letsencrypt' app is currently installed and conflicts with this feature. Please uninstall it first to use the new certificate management interface.",
"ssl_certificate": "SSL certificate",
"confirm_cert_install_LE": "Are you sure you want to install a Let's Encrypt certificate for this domain?",
"confirm_cert_regen_selfsigned": "Are you sure you want to regenerate a self-signed certificate for this domain?",
@ -379,12 +342,10 @@
"install_letsencrypt_cert": "Install a Let's Encrypt certificate",
"manually_renew_letsencrypt_message": "Certificate will be automatically renewed during the last 15 days of validity. You can manually renew it if you want to. (Not recommended).",
"manually_renew_letsencrypt": "Manually renew now",
"meltdown": "You are vulnerable to the <a target=\"_blank\" href=\"https://meltdownattack.com/\">meltdown</a> critical security vulnerability. To fix that, you need to <a href=\"#/update\">update your system</a> then <a href=\"#/tools/reboot\">reboot it</a> to load the new linux kernel.",
"regenerate_selfsigned_cert_message": "If you want, you can regenerate the self-signed certificate.",
"regenerate_selfsigned_cert": "Regenerate self-signed certificate",
"revert_to_selfsigned_cert_message": "If you really want to, you can reinstall a self-signed certificate. (Not recommended)",
"revert_to_selfsigned_cert": "Revert to a self-signed certificate",
"name": "Name",
"purge_user_data_checkbox": "Purge %s's data? (This will remove the content of its home and mail directories.)",
"purge_user_data_warning": "Purging user's data is not reversible. Be sure you know what you're doing!"
}

View file

@ -12,10 +12,6 @@
<h2 class="list-group-item-heading">{{name}} <small>{{id}}</small></h2>
<p class="list-group-item-text">{{uri}}</p>
</a>
{{else}}
<div class="alert alert-warning">
<span class="fa-exclamation-triangle"></span>
{{t 'storages_no'}}
</div>
{{/each}}

View file

@ -21,10 +21,5 @@
</h2>
<p class="list-group-item-text">https://{{url}}</p>
</a>
{{else}}
<div class="alert alert-warning">
<span class="fa-exclamation-triangle"></span>
{{t 'domains_no'}}
</div>
{{/each}}
</div>

View file

@ -9,7 +9,7 @@
<div class="panel panel-default">
<div class="panel-heading">
<h2 class="panel-title" style="display: inline-block; margin-right: 10px;"><span class="fa-fw fa-info-circle"></span> {{t name}}</h2>
<h2 class="panel-title" style="display: inline-block; margin-right: 10px;"><span class="fa-fw fa-info-circle"></span> {{name}}</h2>
{{#if (eq status "running")}}
<button class="btn btn-sm btn-danger pull-right" data-service="{{name}}" data-action="stop">
<span class="fa-stop"></span> {{t 'stop'}}

View file

@ -3,7 +3,7 @@
<a href="#/users" class="visible-xs">&hellip;</a>
<a href="#/users" class="hidden-xs">{{t 'users'}}</a>
<a href="#/groups" class="visible-xs">&hellip;</a>
<a href="#/groups" class="hidden-xs">{{t 'group_permissions'}}</a>
<a href="#/groups" class="hidden-xs">{{t 'groups_and_permissions'}}</a>
<a href="#/groups/create">{{t 'group_new'}}</a>
</div>

120
tests/test_i18n_keys.py Normal file
View file

@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
import os
import re
import glob
import json
import yaml
import subprocess
###############################################################################
# Find used keys in python code #
###############################################################################
def find_expected_string_keys():
# Try to find :
# y18n.t("foo" +) # (the real key is a concatenation of 'foo' with something else)
# y18n.t("foo") or y18n.t('foo', ...) # actual full key
js_p1 = re.compile(r'y18n\.t\(\s*[\"\'](\w+)[\"\']\s*[\,\)]')
js_p2 = re.compile(r'y18n\.t\(\s*[\"\'](\w+)[\"\']\s*\+')
js_files = glob.glob("../src/js/yunohost/controllers/*.js")
js_files.extend(glob.glob("../src/js/yunohost/*.js"))
for js_file in js_files:
content = open(js_file).read()
for m in js_p1.findall(content):
yield m
for m in js_p2.findall(content):
yield m
# In views we have stuff like {{t 'foo' arg}}
views_p1 = re.compile(r'{{t\s*[\"\'](\w+)[\"\']')
# Somes are inside {{ (t 'foo')))
views_p2 = re.compile(r'\(t\s*[\"\'](\w+)[\"\']\)')
views_p3 = re.compile(r't \(concat\s*[\"\'](\w+)[\"\']')
views_p4 = re.compile(r'data-y18n=[\"\'](\w+)[\"\']')
view_files = glob.glob("../src/*.html")
view_files.extend(glob.glob("../src/views/*.ms"))
view_files.extend(glob.glob("../src/views/*/*.ms"))
for view_file in view_files:
content = open(view_file).read()
for m in views_p1.findall(content):
yield m
for m in views_p2.findall(content):
yield m
for m in views_p3.findall(content):
yield m
for m in views_p4.findall(content):
yield m
# App maintenance state
for state in ['maintained', 'orphaned', 'request_adoption', 'request_help','unmaintained']:
yield state
yield state + "_details"
# Service states
for state in ['active', 'disabled', 'enabled', 'inactive']:
yield state
yield "confirm_cert_"
###############################################################################
# Load en locale json keys #
###############################################################################
def keys_defined_for_en():
return json.loads(open("../src/locales/en.json").read()).keys()
###############################################################################
# Compare keys used and keys defined #
###############################################################################
expected_string_keys = set(find_expected_string_keys())
keys_defined = set(keys_defined_for_en())
def test_undefined_i18n_keys():
undefined_keys = expected_string_keys.difference(keys_defined)
undefined_keys = sorted(undefined_keys)
undefined_keys = [k for k in undefined_keys if not k.endswith("_")]
return undefined_keys
if undefined_keys:
raise Exception("Those i18n keys should be defined in en.json:\n"
" - " + "\n - ".join(undefined_keys))
def test_unused_i18n_keys():
unused_keys = keys_defined.difference(expected_string_keys)
unused_keys = sorted(unused_keys)
partial_match = [k for k in unused_keys if any(k.startswith(m) for m in expected_string_keys if m.endswith("_")) ]
unused_keys_2 = []
for k in unused_keys:
if not any(k.startswith(m) for m in expected_string_keys if m.endswith("_")):
unused_keys_2.append(k)
return unused_keys_2
if unused_keys:
raise Exception("Those i18n keys appears unused:\n"
" - " + "\n - ".join(unused_keys))
print("--------- undefined")
print()
print(test_undefined_i18n_keys())
print("--------- unused")
print()
print(test_unused_i18n_keys())