mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
Merge pull request #288 from YunoHost/string-cleaning
String definition / usage tests + cleaning of stale strings
This commit is contained in:
commit
3007925281
8 changed files with 126 additions and 117 deletions
|
@ -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 {
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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 %s’s 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!"
|
||||
}
|
||||
|
|
|
@ -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}}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'}}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<a href="#/users" class="visible-xs">…</a>
|
||||
<a href="#/users" class="hidden-xs">{{t 'users'}}</a>
|
||||
<a href="#/groups" class="visible-xs">…</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
120
tests/test_i18n_keys.py
Normal 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())
|
Loading…
Reference in a new issue